From 37b7f3d7cdc8ee787c9ddacfc7a258adb70330c1 Mon Sep 17 00:00:00 2001 From: burnettk Date: Fri, 18 Nov 2022 10:03:32 -0500 Subject: [PATCH 1/3] Squashed 'SpiffWorkflow/' changes from 580939cc..cd4da465 cd4da465 Merge pull request #264 from sartography/bugfix/dmn-equality-with-boolean 414a59eb disambiguate DMN expressions eea53c91 Merge pull request #263 from sartography/feature/cleanup-task-completion d248d5b1 execute postscript before other complete hook tasks c09f1a90 streamline predict & remove some duplicated calls to it 64c21791 remove duplicate calls to update 4ca1076d move task update to _on_complete to ensure data is copied consistently after task related activities are done d037a7eb small changes for readability 025bc30f Quick patch -- is_executable needs to be accurate immediately. 14d3d8c3 Merge pull request #262 from sartography/feature/parser_info_features 849c223e We are jumping through a lot of complex xml parsing in SpiffWorkflow-Backend because we need to know some basic information about a BPMN process at the moment it is saved. Rather than do that work in the backend, it seems better to have SpiffWorkflow handle parsing the xml and providing a bit of metadata, including: git-subtree-dir: SpiffWorkflow git-subtree-split: cd4da465e125ca1ae1b57d227bfa324d9d4c507c --- SpiffWorkflow/bpmn/parser/BpmnParser.py | 41 +++++++ SpiffWorkflow/bpmn/parser/ProcessParser.py | 32 +++++- SpiffWorkflow/bpmn/specs/MultiInstanceTask.py | 100 +++++++----------- SpiffWorkflow/bpmn/specs/SubWorkflowTask.py | 2 - SpiffWorkflow/bpmn/specs/UnstructuredJoin.py | 2 - SpiffWorkflow/dmn/engine/DMNEngine.py | 3 +- SpiffWorkflow/specs/ExclusiveChoice.py | 2 - SpiffWorkflow/specs/MultiChoice.py | 2 - SpiffWorkflow/specs/MultiInstance.py | 2 - SpiffWorkflow/specs/SubWorkflow.py | 28 ++--- SpiffWorkflow/specs/ThreadSplit.py | 2 - SpiffWorkflow/specs/base.py | 47 +++----- SpiffWorkflow/spiff/specs/spiff_task.py | 2 +- SpiffWorkflow/task.py | 31 +++--- .../bpmn/BpmnWorkflowTestCase.py | 6 +- tests/SpiffWorkflow/bpmn/CollaborationTest.py | 16 ++- tests/SpiffWorkflow/bpmn/SwimLaneTest.py | 6 ++ tests/SpiffWorkflow/camunda/BaseTestCase.py | 10 +- .../camunda/MultiInstanceParallelArrayTest.py | 8 +- .../camunda/ResetTokenMIParallelTest.py | 11 +- .../camunda/StartMessageEventTest.py | 10 ++ .../data/spiff/control-flow/recursion.path | 2 +- .../control-flow/subworkflow_to_join.path | 2 +- tests/SpiffWorkflow/specs/SubWorkflowTest.py | 18 ++-- tests/SpiffWorkflow/util.py | 3 - 25 files changed, 215 insertions(+), 173 deletions(-) diff --git a/SpiffWorkflow/bpmn/parser/BpmnParser.py b/SpiffWorkflow/bpmn/parser/BpmnParser.py index e6783e05..1880eb21 100644 --- a/SpiffWorkflow/bpmn/parser/BpmnParser.py +++ b/SpiffWorkflow/bpmn/parser/BpmnParser.py @@ -120,6 +120,8 @@ class BpmnParser(object): self.process_parsers_by_name = {} self.collaborations = {} self.process_dependencies = set() + self.messages = {} + self.correlations = {} def _get_parser_class(self, tag): if tag in self.OVERRIDE_PARSER_CLASSES: @@ -179,6 +181,8 @@ class BpmnParser(object): self._add_processes(bpmn, filename) self._add_collaborations(bpmn) + self._add_messages(bpmn) + self._add_correlations(bpmn) def _add_processes(self, bpmn, filename=None): for process in bpmn.xpath('.//bpmn:process', namespaces=self.namespaces): @@ -192,6 +196,43 @@ class BpmnParser(object): name = collaboration.get('id') self.collaborations[name] = [ participant.get('processRef') for participant in collaboration_xpath('.//bpmn:participant') ] + def _add_messages(self, bpmn): + for message in bpmn.xpath('.//bpmn:message', namespaces=self.namespaces): + if message.attrib.get("id") is None: + raise ValidationException( + "Message identifier is missing from bpmn xml" + ) + self.messages[message.attrib.get("id")] = message.attrib.get("name") + + def _add_correlations(self, bpmn): + for correlation in bpmn.xpath('.//bpmn:correlationProperty', namespaces=self.namespaces): + correlation_identifier = correlation.attrib.get("id") + if correlation_identifier is None: + raise ValidationException( + "Correlation identifier is missing from bpmn xml" + ) + correlation_property_retrieval_expressions = correlation.xpath( + "//bpmn:correlationPropertyRetrievalExpression", namespaces = self.namespaces) + if not correlation_property_retrieval_expressions: + raise ValidationException( + f"Correlation is missing correlation property retrieval expressions: {correlation_identifier}" + ) + retrieval_expressions = [] + for cpre in correlation_property_retrieval_expressions: + message_model_identifier = cpre.attrib.get("messageRef") + if message_model_identifier is None: + raise ValidationException( + f"Message identifier is missing from correlation property: {correlation_identifier}" + ) + children = cpre.getchildren() + expression = children[0].text if len(children) > 0 else None + retrieval_expressions.append({"messageRef": message_model_identifier, + "expression": expression}) + self.correlations[correlation_identifier] = { + "name": correlation.attrib.get("name"), + "retrieval_expressions": retrieval_expressions + } + def _find_dependencies(self, process): """Locate all calls to external BPMN, and store their ids in our list of dependencies""" for call_activity in process.xpath('.//bpmn:callActivity', namespaces=self.namespaces): diff --git a/SpiffWorkflow/bpmn/parser/ProcessParser.py b/SpiffWorkflow/bpmn/parser/ProcessParser.py index 6fd3a427..c6f6b665 100644 --- a/SpiffWorkflow/bpmn/parser/ProcessParser.py +++ b/SpiffWorkflow/bpmn/parser/ProcessParser.py @@ -44,7 +44,7 @@ class ProcessParser(NodeParser): self.parsed_nodes = {} self.lane = lane self.spec = None - self.process_executable = True + self.process_executable = self.is_executable() def get_name(self): """ @@ -52,6 +52,35 @@ class ProcessParser(NodeParser): """ return self.node.get('name', default=self.get_id()) + def has_lanes(self) -> bool: + """Returns true if this process has one or more named lanes """ + elements = self.xpath("//bpmn:lane") + for el in elements: + if el.get("name"): + return True + return False + + def is_executable(self) -> bool: + return self.node.get('isExecutable', 'true') == 'true' + + def start_messages(self): + """ This returns a list of messages that would cause this + process to start. """ + messages = [] + message_event_definitions = self.xpath( + "//bpmn:startEvent/bpmn:messageEventDefinition") + for message_event_definition in message_event_definitions: + message_model_identifier = message_event_definition.attrib.get( + "messageRef" + ) + if message_model_identifier is None: + raise ValidationException( + "Could not find messageRef from message event definition: {message_event_definition}" + ) + messages.append(message_model_identifier) + + return messages + def parse_node(self, node): """ Parses the specified child task node, and returns the task spec. This @@ -72,7 +101,6 @@ class ProcessParser(NodeParser): def _parse(self): # here we only look in the top level, We will have another # bpmn:startEvent if we have a subworkflow task - self.process_executable = self.node.get('isExecutable', 'true') == 'true' start_node_list = self.xpath('./bpmn:startEvent') if not start_node_list and self.process_executable: raise ValidationException("No start event found", node=self.node, filename=self.filename) diff --git a/SpiffWorkflow/bpmn/specs/MultiInstanceTask.py b/SpiffWorkflow/bpmn/specs/MultiInstanceTask.py index f0ceb3de..6a81c26f 100644 --- a/SpiffWorkflow/bpmn/specs/MultiInstanceTask.py +++ b/SpiffWorkflow/bpmn/specs/MultiInstanceTask.py @@ -81,12 +81,6 @@ class MultiInstanceTask(TaskSpec): TaskSpec.__init__(self, wf_spec, name, **kwargs) - -# DO NOT OVERRIDE THE SPEC TYPE. -# @property -# def spec_type(self): -# return 'MultiInstance Task' - def _find_my_task(self, task): for thetask in task.workflow.task_tree: if thetask.thread_id != task.thread_id: @@ -113,17 +107,6 @@ class MultiInstanceTask(TaskSpec): new_task.triggered = True output._predict(new_task) - def _check_inputs(self, my_task): - if self.collection is None: - return - # look for variable in context, if we don't find it, default to 1 - variable = valueof(my_task, self.times, 1) - if self.times.name == self.collection.name and type(variable) == type([]): - raise WorkflowTaskExecException(my_task, - 'If we are updating a collection,' - ' then the collection must be a ' - 'dictionary.') - def _get_loop_completion(self,my_task): if not self.completioncondition == None: terminate = my_task.workflow.script_engine.evaluate(my_task,self.completioncondition) @@ -154,17 +137,6 @@ class MultiInstanceTask(TaskSpec): return len(variable.keys()) return 1 # we shouldn't ever get here, but just in case return a sane value. - def _get_current_var(self, my_task, pos): - variable = valueof(my_task, self.times, 1) - if is_number(variable): - return pos - if isinstance(variable,list) and len(variable) >= pos: - return variable[pos - 1] - elif isinstance(variable,dict) and len(list(variable.keys())) >= pos: - return variable[list(variable.keys())[pos - 1]] - else: - return pos - def _get_predicted_outputs(self, my_task): split_n = self._get_count(my_task) @@ -418,52 +390,60 @@ class MultiInstanceTask(TaskSpec): if my_task.task_spec.prevtaskclass in classes.keys() and not terminate: super()._on_complete_hook(my_task) - def _merge_element_variable(self,my_task,collect,runtimes,colvarname): - # if we are updating the same collection as was our loopcardinality - # then all the keys should be there and we can use the sorted keylist - # if not, we use an integer - we should be guaranteed that the - # collection is a dictionary + def _check_inputs(self, my_task): + if self.collection is None: + return + # look for variable in context, if we don't find it, default to 1 + variable = valueof(my_task, self.times, 1) + if self.times.name == self.collection.name and type(variable) == type([]): + raise WorkflowTaskExecException(my_task, + 'If we are updating a collection, then the collection must be a dictionary.') + + def _get_current_var(self, my_task, pos): + variable = valueof(my_task, self.times, 1) + if is_number(variable): + return pos + if isinstance(variable,list) and len(variable) >= pos: + return variable[pos - 1] + elif isinstance(variable,dict) and len(list(variable.keys())) >= pos: + return variable[list(variable.keys())[pos - 1]] + else: + return pos + + def _merge_element_variable(self, my_task, collect, runtimes): if self.collection is not None and self.times.name == self.collection.name: + # Update an existing collection (we used the collection as the cardinality) keys = list(collect.keys()) if len(keys) < runtimes: msg = f"There is a mismatch between runtimes and the number " \ f"items in the collection, please check for empty " \ f"collection {self.collection.name}." raise WorkflowTaskExecException(my_task, msg) - runtimesvar = keys[runtimes - 1] else: + # Use an integer (for arrays) runtimesvar = runtimes if self.elementVar in my_task.data and isinstance(my_task.data[self.elementVar], dict): - collect[str(runtimesvar)] = DeepMerge.merge(collect.get(runtimesvar, {}), - copy.copy(my_task.data[self.elementVar])) + collect[str(runtimesvar)] = DeepMerge.merge( + collect.get(runtimesvar, {}), + copy.copy(my_task.data[self.elementVar]) + ) - my_task.data = DeepMerge.merge(my_task.data, - gendict(colvarname.split('/'), collect)) + def _update_sibling_data(self, my_task, runtimes, runcount, colvarname, collect): - - def _update_sibling_data(self,my_task,runtimes,runcount,colvarname,collect): if (runtimes < runcount) and not my_task.terminate_current_loop and self.loopTask: my_task._set_state(TaskState.READY) my_task._set_internal_data(runtimes=runtimes + 1) my_task.data[self.elementVar] = self._get_current_var(my_task, runtimes + 1) - element_var_data = None else: - # The element var data should not be passed on to children - # but will add this back onto this task later. - element_var_data = my_task.data.pop(self.elementVar, None) + my_task.data.pop(self.elementVar, None) - # if this is a parallel mi - then update all siblings with the - # current data - if not self.isSequential: - for task in my_task.parent.children: - task.data = DeepMerge.merge( - task.data, - gendict(colvarname.split('/'), - collect) - ) - return element_var_data + for task in my_task.parent.children: + task.data = DeepMerge.merge( + task.data, + gendict(colvarname.split('/'), collect) + ) def _on_complete_hook(self, my_task): # do special stuff for non-user tasks @@ -486,9 +466,9 @@ class MultiInstanceTask(TaskSpec): collect = valueof(my_task, self.collection, {}) - self._merge_element_variable(my_task,collect,runtimes,colvarname) + self._merge_element_variable(my_task, collect, runtimes) - element_var_data = self._update_sibling_data(my_task,runtimes,runcount,colvarname,collect) + self._update_sibling_data(my_task, runtimes, runcount, colvarname, collect) # please see MultiInstance code for previous version outputs = [] @@ -497,14 +477,6 @@ class MultiInstanceTask(TaskSpec): if not isinstance(my_task.task_spec,SubWorkflowTask): my_task._sync_children(outputs, TaskState.FUTURE) - for child in my_task.children: - child.task_spec._update(child) - - # If removed, add the element_var_data back onto this task, after - # updating the children. - if(element_var_data): - my_task.data[self.elementVar] = element_var_data - def serialize(self, serializer): return serializer.serialize_multi_instance(self) diff --git a/SpiffWorkflow/bpmn/specs/SubWorkflowTask.py b/SpiffWorkflow/bpmn/specs/SubWorkflowTask.py index 6f616f1d..978ef7f5 100644 --- a/SpiffWorkflow/bpmn/specs/SubWorkflowTask.py +++ b/SpiffWorkflow/bpmn/specs/SubWorkflowTask.py @@ -66,8 +66,6 @@ class SubWorkflowTask(BpmnSpecMixin): def _on_complete_hook(self, my_task): BpmnSpecMixin._on_complete_hook(self, my_task) - for child in my_task.children: - child.task_spec._update(child) def _on_cancel(self, my_task): subworkflow = my_task.workflow.get_subprocess(my_task) diff --git a/SpiffWorkflow/bpmn/specs/UnstructuredJoin.py b/SpiffWorkflow/bpmn/specs/UnstructuredJoin.py index 5d8f0ca6..d2e45be3 100644 --- a/SpiffWorkflow/bpmn/specs/UnstructuredJoin.py +++ b/SpiffWorkflow/bpmn/specs/UnstructuredJoin.py @@ -146,8 +146,6 @@ class UnstructuredJoin(Join, BpmnSpecMixin): def _update_hook(self, my_task): - if my_task._is_predicted(): - self._predict(my_task) if not my_task.parent._is_finished(): return diff --git a/SpiffWorkflow/dmn/engine/DMNEngine.py b/SpiffWorkflow/dmn/engine/DMNEngine.py index c0e8dfd1..d2b10532 100644 --- a/SpiffWorkflow/dmn/engine/DMNEngine.py +++ b/SpiffWorkflow/dmn/engine/DMNEngine.py @@ -95,7 +95,8 @@ class DMNEngine: # If we get here, we need to check whether the match expression includes # an operator or if can use '==' needs_eq = self.needs_eq(script_engine, match_expr) - expr = input_expr + ' == ' + match_expr if needs_eq else input_expr + match_expr + # Disambiguate cases like a == 0 == True when we add '==' + expr = f'({input_expr}) == ({match_expr})' if needs_eq else input_expr + match_expr return script_engine.evaluate(task, expr) @staticmethod diff --git a/SpiffWorkflow/specs/ExclusiveChoice.py b/SpiffWorkflow/specs/ExclusiveChoice.py index c0290c12..00978d7d 100644 --- a/SpiffWorkflow/specs/ExclusiveChoice.py +++ b/SpiffWorkflow/specs/ExclusiveChoice.py @@ -88,8 +88,6 @@ class ExclusiveChoice(MultiChoice): f'No conditions satisfied for {my_task.task_spec.name}') my_task._sync_children([output], TaskState.FUTURE) - for child in my_task.children: - child.task_spec._update(child) def serialize(self, serializer): return serializer.serialize_exclusive_choice(self) diff --git a/SpiffWorkflow/specs/MultiChoice.py b/SpiffWorkflow/specs/MultiChoice.py index 26c94899..3f1cd36f 100644 --- a/SpiffWorkflow/specs/MultiChoice.py +++ b/SpiffWorkflow/specs/MultiChoice.py @@ -134,8 +134,6 @@ class MultiChoice(TaskSpec): outputs.append(self._wf_spec.get_task_spec_from_name(output)) my_task._sync_children(outputs, TaskState.FUTURE) - for child in my_task.children: - child.task_spec._update(child) def serialize(self, serializer): return serializer.serialize_multi_choice(self) diff --git a/SpiffWorkflow/specs/MultiInstance.py b/SpiffWorkflow/specs/MultiInstance.py index d7ca6b0b..a1abf3f0 100644 --- a/SpiffWorkflow/specs/MultiInstance.py +++ b/SpiffWorkflow/specs/MultiInstance.py @@ -102,8 +102,6 @@ class MultiInstance(TaskSpec): def _on_complete_hook(self, my_task): outputs = self._get_predicted_outputs(my_task) my_task._sync_children(outputs, TaskState.FUTURE) - for child in my_task.children: - child.task_spec._update(child) def serialize(self, serializer): return serializer.serialize_multi_instance(self) diff --git a/SpiffWorkflow/specs/SubWorkflow.py b/SpiffWorkflow/specs/SubWorkflow.py index 197a6912..318c224b 100644 --- a/SpiffWorkflow/specs/SubWorkflow.py +++ b/SpiffWorkflow/specs/SubWorkflow.py @@ -76,12 +76,14 @@ class SubWorkflow(TaskSpec): self, 'File does not exist: %s' % self.file) def _predict_hook(self, my_task): + # Modifying the task spec is a TERRIBLE idea, but if we don't do it, sync_children won't work outputs = [task.task_spec for task in my_task.children] for output in self.outputs: if output not in outputs: outputs.insert(0, output) if my_task._is_definite(): - my_task._sync_children(outputs, TaskState.FUTURE) + # This prevents errors with sync children + my_task._sync_children(outputs, TaskState.LIKELY) else: my_task._sync_children(outputs, my_task.state) @@ -107,10 +109,7 @@ class SubWorkflow(TaskSpec): def _integrate_subworkflow_tree(self, my_task, subworkflow): # Integrate the tree of the subworkflow into the tree of this workflow. - my_task._sync_children(self.outputs, TaskState.FUTURE) - for child in my_task.children: - child.task_spec._update(child) - child._inherit_data() + my_task._sync_children(self.outputs, TaskState.LIKELY) for child in subworkflow.task_tree.children: my_task.children.insert(0, child) child.parent = my_task @@ -121,10 +120,18 @@ class SubWorkflow(TaskSpec): for child in subworkflow.task_tree.children: for assignment in self.in_assign: assignment.assign(my_task, child) - - self._predict(my_task) - for child in subworkflow.task_tree.children: child.task_spec._update(child) + # Instead of completing immediately, we'll wait for the subworkflow to complete + my_task._set_state(TaskState.WAITING) + + def _update_hook(self, my_task): + subworkflow = my_task._get_internal_data('subworkflow') + if subworkflow is None: + # On the first update, we have to create the subworkflow + super()._update_hook(my_task) + elif subworkflow.is_completed(): + # Then wait until it finishes to complete + my_task.complete() def _on_subworkflow_completed(self, subworkflow, my_task): # Assign variables, if so requested. @@ -138,11 +145,6 @@ class SubWorkflow(TaskSpec): # Alright, abusing that hook is just evil but it works. child.task_spec._update_hook(child) - def _on_complete_hook(self, my_task): - for child in my_task.children: - if isinstance(child.task_spec, StartTask): - child.task_spec._update(child) - def serialize(self, serializer): return serializer.serialize_sub_workflow(self) diff --git a/SpiffWorkflow/specs/ThreadSplit.py b/SpiffWorkflow/specs/ThreadSplit.py index b7703986..d25e8388 100644 --- a/SpiffWorkflow/specs/ThreadSplit.py +++ b/SpiffWorkflow/specs/ThreadSplit.py @@ -133,8 +133,6 @@ class ThreadSplit(TaskSpec): for i in range(split_n): outputs.append(self.thread_starter) my_task._sync_children(outputs, TaskState.FUTURE) - for child in my_task.children: - child.task_spec._update(child) def serialize(self, serializer): return serializer.serialize_thread_split(self) diff --git a/SpiffWorkflow/specs/base.py b/SpiffWorkflow/specs/base.py index f6fb4df2..6a6b10c3 100644 --- a/SpiffWorkflow/specs/base.py +++ b/SpiffWorkflow/specs/base.py @@ -242,24 +242,19 @@ class TaskSpec(object): :type looked_ahead: integer :param looked_ahead: The depth of the predicted path so far. """ - if my_task._is_finished(): - return if seen is None: seen = [] - elif self in seen: - return - if not my_task._is_finished(): - self._predict_hook(my_task) + + self._predict_hook(my_task) if not my_task._is_definite(): - if looked_ahead + 1 >= self.lookahead: - return seen.append(self) + look_ahead = my_task._is_definite() or looked_ahead + 1 < self.lookahead for child in my_task.children: - child.task_spec._predict(child, seen[:], looked_ahead + 1) + if not child._is_finished() and child not in seen and look_ahead: + child.task_spec._predict(child, seen[:], looked_ahead + 1) def _predict_hook(self, my_task): - # If the task's status is not predicted, we default to FUTURE - # for all it's outputs. + # If the task's status is not predicted, we default to FUTURE for all it's outputs. # Otherwise, copy my own state to the children. if my_task._is_definite(): best_state = TaskState.FUTURE @@ -278,6 +273,12 @@ class TaskSpec(object): completes it makes sure to call this method so we can react. """ my_task._inherit_data() + # We were doing this in _update_hook, but to me that seems inconsistent with the spirit + # of the hook functions. Moving it here allows removal of some repeated calls (overridden + # hook methods still need to do these things) + if my_task._is_predicted(): + self._predict(my_task) + self.entered_event.emit(my_task.workflow, my_task) self._update_hook(my_task) def _update_hook(self, my_task): @@ -290,11 +291,8 @@ class TaskSpec(object): Returning non-False will cause the task to go into READY. Returning any other value will cause no action. """ - if my_task._is_predicted(): - self._predict(my_task) - if not my_task.parent._is_finished(): - return - self.entered_event.emit(my_task.workflow, my_task) + # If this actually did what the documentation said (returned a value indicating + # that the task was ready), then a lot of things might be easier. my_task._ready() def _on_ready(self, my_task): @@ -387,21 +385,14 @@ class TaskSpec(object): """ assert my_task is not None - if my_task.workflow.debug: - print("Executing %s: %s (%s)" % ( - my_task.task_spec.__class__.__name__, - my_task.get_name(), my_task.get_description())) - # We have to set the last task here, because the on_complete_hook # of a loopback task may overwrite what the last_task will be. my_task.workflow.last_task = my_task self._on_complete_hook(my_task) + for child in my_task.children: + child.task_spec._update(child) my_task.workflow._task_completed_notify(my_task) - if my_task.workflow.debug: - if hasattr(my_task.workflow, "outer_workflow"): - my_task.workflow.outer_workflow.task_tree.dump() - self.completed_event.emit(my_task.workflow, my_task) return True @@ -414,9 +405,7 @@ class TaskSpec(object): :rtype: bool :returns: True on success, False otherwise. """ - # If we have more than one output, implicitly split. - for child in my_task.children: - child.task_spec._update(child) + pass @abstractmethod def serialize(self, serializer, **kwargs): @@ -478,8 +467,6 @@ class TaskSpec(object): :rtype: TaskSpec :returns: The task specification instance. """ - print(s_state) - print(wf_spec) out = cls(wf_spec,s_state.get('name')) out.id = s_state.get('id') out.name = s_state.get('name') diff --git a/SpiffWorkflow/spiff/specs/spiff_task.py b/SpiffWorkflow/spiff/specs/spiff_task.py index 04b89db7..bb25823a 100644 --- a/SpiffWorkflow/spiff/specs/spiff_task.py +++ b/SpiffWorkflow/spiff/specs/spiff_task.py @@ -39,6 +39,6 @@ class SpiffBpmnTask(BpmnSpecMixin): self.execute_script(my_task, self.prescript) def _on_complete_hook(self, my_task): - super()._on_complete_hook(my_task) if self.postscript is not None: self.execute_script(my_task, self.postscript) + super()._on_complete_hook(my_task) \ No newline at end of file diff --git a/SpiffWorkflow/task.py b/SpiffWorkflow/task.py index 458c6b53..a214eb5b 100644 --- a/SpiffWorkflow/task.py +++ b/SpiffWorkflow/task.py @@ -522,46 +522,42 @@ class Task(object, metaclass=DeprecatedMetaTask): """ if task_specs is None: raise ValueError('"task_specs" argument is None') - add = task_specs[:] + new_children = task_specs[:] # If a child task_spec is also an ancestor, we are looping back, # replace those specs with a loopReset task. root_task = self._get_root() - for index, task_spec in enumerate(add): + for index, task_spec in enumerate(new_children): ancestor_task = self._find_ancestor(task_spec) if ancestor_task and ancestor_task != root_task: destination = ancestor_task new_spec = self.workflow.get_reset_task_spec(destination) new_spec.outputs = [] new_spec.inputs = task_spec.inputs - add[index] = new_spec + new_children[index] = new_spec # Create a list of all children that are no longer needed. - remove = [] + unneeded_children = [] for child in self.children: # Triggered tasks are never removed. if child.triggered: continue - # Check whether the task needs to be removed. - if child.task_spec in add: - add.remove(child.task_spec) + # If the task already exists, remove it from to-be-added + if child.task_spec in new_children: + new_children.remove(child.task_spec) + # We should set the state here but that breaks everything continue - # Non-predicted tasks must not be removed, so they HAVE to be in - # the given task spec list. + # Definite tasks must not be removed, so they HAVE to be in the given task spec list. if child._is_definite(): - raise WorkflowException(self.task_spec, - 'removal of non-predicted child %s' % - repr(child)) - remove.append(child) - - + raise WorkflowException(self.task_spec, f'removal of non-predicted child {child}') + unneeded_children.append(child) # Remove and add the children accordingly. - for child in remove: + for child in unneeded_children: self.children.remove(child) - for task_spec in add: + for task_spec in new_children: self._add_child(task_spec, state) def _set_likely_task(self, task_specs): @@ -574,7 +570,6 @@ class Task(object, metaclass=DeprecatedMetaTask): if child._is_definite(): continue child._set_state(TaskState.LIKELY) - return def _is_descendant_of(self, parent): """ diff --git a/tests/SpiffWorkflow/bpmn/BpmnWorkflowTestCase.py b/tests/SpiffWorkflow/bpmn/BpmnWorkflowTestCase.py index e2864919..9b7865bd 100644 --- a/tests/SpiffWorkflow/bpmn/BpmnWorkflowTestCase.py +++ b/tests/SpiffWorkflow/bpmn/BpmnWorkflowTestCase.py @@ -19,11 +19,15 @@ class BpmnWorkflowTestCase(unittest.TestCase): serializer = BpmnWorkflowSerializer(wf_spec_converter) - def load_workflow_spec(self, filename, process_name, validate=True): + def get_parser(self, filename, validate=True): f = os.path.join(os.path.dirname(__file__), 'data', filename) validator = BpmnValidator() if validate else None parser = TestBpmnParser(validator=validator) parser.add_bpmn_files_by_glob(f) + return parser + + def load_workflow_spec(self, filename, process_name, validate=True): + parser = self.get_parser(filename, validate) top_level_spec = parser.get_spec(process_name) subprocesses = parser.get_subprocess_specs(process_name) return top_level_spec, subprocesses diff --git a/tests/SpiffWorkflow/bpmn/CollaborationTest.py b/tests/SpiffWorkflow/bpmn/CollaborationTest.py index e1361dd8..a14d9f99 100644 --- a/tests/SpiffWorkflow/bpmn/CollaborationTest.py +++ b/tests/SpiffWorkflow/bpmn/CollaborationTest.py @@ -6,10 +6,22 @@ from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase class CollaborationTest(BpmnWorkflowTestCase): + def testParserProvidesInfoOnMessagesAndCorrelations(self): + parser = self.get_parser('collaboration.bpmn') + self.assertEqual(list(parser.messages.keys()), ['love_letter', 'love_letter_response']) + self.assertEqual(parser.correlations, + {'lover_name': {'name': "Lover's Name", + 'retrieval_expressions': [ + {'expression': 'lover_name', + 'messageRef': 'love_letter'}, + {'expression': 'from_name', + 'messageRef': 'love_letter_response'}]}} + ) + def testCollaboration(self): spec, subprocesses = self.load_collaboration('collaboration.bpmn', 'my_collaboration') - + # Only executable processes should be started self.assertIn('process_buddy', subprocesses) self.assertNotIn('random_person_process', subprocesses) @@ -122,4 +134,4 @@ class CollaborationTest(BpmnWorkflowTestCase): start = self.workflow.get_tasks_from_spec_name('Start')[0] start.data['lover_name'] = 'Peggy' self.workflow.do_engine_steps() - self.save_restore() \ No newline at end of file + self.save_restore() diff --git a/tests/SpiffWorkflow/bpmn/SwimLaneTest.py b/tests/SpiffWorkflow/bpmn/SwimLaneTest.py index 41638273..12bbeddd 100644 --- a/tests/SpiffWorkflow/bpmn/SwimLaneTest.py +++ b/tests/SpiffWorkflow/bpmn/SwimLaneTest.py @@ -19,6 +19,12 @@ class SwimLaneTest(BpmnWorkflowTestCase): spec, subprocesses = self.load_workflow_spec('lanes.bpmn','lanes') self.workflow = BpmnWorkflow(spec, subprocesses) + def testBpmnParserKnowsLanesExist(self): + parser = self.get_parser('lanes.bpmn') + self.assertTrue(parser.get_process_parser('lanes').has_lanes()) + parser = self.get_parser('random_fact.bpmn') + self.assertFalse(parser.get_process_parser('random_fact').has_lanes()) + def testRunThroughHappy(self): self.workflow.do_engine_steps() diff --git a/tests/SpiffWorkflow/camunda/BaseTestCase.py b/tests/SpiffWorkflow/camunda/BaseTestCase.py index b41a8aab..67d9c590 100644 --- a/tests/SpiffWorkflow/camunda/BaseTestCase.py +++ b/tests/SpiffWorkflow/camunda/BaseTestCase.py @@ -23,13 +23,17 @@ class BaseTestCase(BpmnWorkflowTestCase): serializer = BpmnWorkflowSerializer(wf_spec_converter) - def load_workflow_spec(self, filename, process_name, dmn_filename=None): - bpmn = os.path.join(os.path.dirname(__file__), 'data', filename) + def get_parser(self, filename, dmn_filename=None): + f = os.path.join(os.path.dirname(__file__), 'data', filename) parser = CamundaParser() - parser.add_bpmn_files_by_glob(bpmn) + parser.add_bpmn_files_by_glob(f) if dmn_filename is not None: dmn = os.path.join(os.path.dirname(__file__), 'data', 'dmn', dmn_filename) parser.add_dmn_files_by_glob(dmn) + return parser + + def load_workflow_spec(self, filename, process_name, dmn_filename=None): + parser = self.get_parser(filename, dmn_filename) top_level_spec = parser.get_spec(process_name) subprocesses = parser.get_subprocess_specs(process_name) return top_level_spec, subprocesses diff --git a/tests/SpiffWorkflow/camunda/MultiInstanceParallelArrayTest.py b/tests/SpiffWorkflow/camunda/MultiInstanceParallelArrayTest.py index 42a616f9..14373b0c 100644 --- a/tests/SpiffWorkflow/camunda/MultiInstanceParallelArrayTest.py +++ b/tests/SpiffWorkflow/camunda/MultiInstanceParallelArrayTest.py @@ -70,11 +70,9 @@ class MultiInstanceParallelArrayTest(BaseTestCase): {"CurrentFamilyMember": {"Birthdate": "10/05/1985" + str(x)}}) self.workflow.do_engine_steps() self.workflow.complete_task_from_id(task.id) - # The data should still be available on the current task. - self.assertEqual({'FirstName': "The Funk #%i" % x, - 'Birthdate': '10/05/1985' + str(x)}, - self.workflow.get_task(task.id) - .data['CurrentFamilyMember']) + # We used to check that the current data variable was available in the task, + # but there's no reason to preserve it after the task completes. We removed it + # in some cases and left it in others, which just adds to the confusion. self.workflow.do_engine_steps() if save_restore: self.reload_save_restore() diff --git a/tests/SpiffWorkflow/camunda/ResetTokenMIParallelTest.py b/tests/SpiffWorkflow/camunda/ResetTokenMIParallelTest.py index 39207181..75f2df36 100644 --- a/tests/SpiffWorkflow/camunda/ResetTokenMIParallelTest.py +++ b/tests/SpiffWorkflow/camunda/ResetTokenMIParallelTest.py @@ -44,8 +44,7 @@ class ResetTokenTestMIParallel(BaseTestCase): self.workflow.do_engine_steps() if save_restore: self.save_restore() - self.assertEqual({'current': {'A': 'y'}, - 'do_step': 'Yes', + self.assertEqual({'do_step': 'Yes', 'output': {'1': {'A': 'x'}, '2': {'A': 'y'}, '3': {'A': 'z'}}}, self.workflow.last_task.data) @@ -66,8 +65,7 @@ class ResetTokenTestMIParallel(BaseTestCase): self.assertTrue(self.workflow.is_completed()) - self.assertEqual({'current': {'A': 'x'}, - 'do_step': 'Yes', + self.assertEqual({'do_step': 'Yes', 'C': 'c', 'output': {'1': {'A': 'a1'}, '2': {'A': 'y'}, @@ -75,11 +73,6 @@ class ResetTokenTestMIParallel(BaseTestCase): self.workflow.last_task.data) - - - - - def suite(): return unittest.TestLoader().loadTestsFromTestCase(ResetTokenTestMIParallel) diff --git a/tests/SpiffWorkflow/camunda/StartMessageEventTest.py b/tests/SpiffWorkflow/camunda/StartMessageEventTest.py index a96d2e42..5dd81a06 100644 --- a/tests/SpiffWorkflow/camunda/StartMessageEventTest.py +++ b/tests/SpiffWorkflow/camunda/StartMessageEventTest.py @@ -14,6 +14,16 @@ class StartMessageTest(BaseTestCase): self.spec, self.subprocesses = self.load_workflow_spec('message_test.bpmn', 'ThrowCatch') self.workflow = BpmnWorkflow(self.spec, self.subprocesses) + def testParserCanReturnStartMessages(self): + parser = self.get_parser('message_test.bpmn') + self.assertEqual( + parser.process_parsers['ThrowCatch'].start_messages(), ['Message_1rkbi27']) + + parser = self.get_parser('random_fact.bpmn') + self.assertEqual( + parser.process_parsers['random_fact'].start_messages(), []) + + def testRunThroughHappy(self): self.actual_test(save_restore=False) diff --git a/tests/SpiffWorkflow/data/spiff/control-flow/recursion.path b/tests/SpiffWorkflow/data/spiff/control-flow/recursion.path index 4b406b8f..de57f21f 100644 --- a/tests/SpiffWorkflow/data/spiff/control-flow/recursion.path +++ b/tests/SpiffWorkflow/data/spiff/control-flow/recursion.path @@ -1,11 +1,11 @@ Start first excl_choice_1 - sub_workflow_1 Start first excl_choice_1 last End + sub_workflow_1 last End diff --git a/tests/SpiffWorkflow/data/spiff/control-flow/subworkflow_to_join.path b/tests/SpiffWorkflow/data/spiff/control-flow/subworkflow_to_join.path index e3a1fde8..527c4fd6 100644 --- a/tests/SpiffWorkflow/data/spiff/control-flow/subworkflow_to_join.path +++ b/tests/SpiffWorkflow/data/spiff/control-flow/subworkflow_to_join.path @@ -1,10 +1,10 @@ Start first - sub_workflow_1 Start first last End + sub_workflow_1 second join last diff --git a/tests/SpiffWorkflow/specs/SubWorkflowTest.py b/tests/SpiffWorkflow/specs/SubWorkflowTest.py index 5baacf99..0590192f 100644 --- a/tests/SpiffWorkflow/specs/SubWorkflowTest.py +++ b/tests/SpiffWorkflow/specs/SubWorkflowTest.py @@ -38,6 +38,8 @@ class TaskSpecTest(unittest.TestCase): def do_next_unique_task(self, name): # This method asserts that there is only one ready task! The specified # one - and then completes it + for task in self.workflow.get_tasks(TaskState.WAITING): + task.task_spec._update(task) ready_tasks = self.workflow.get_tasks(TaskState.READY) self.assertEqual(1, len(ready_tasks)) task = ready_tasks[0] @@ -58,12 +60,13 @@ class TaskSpecTest(unittest.TestCase): self.load_workflow_spec('data', 'block_to_subworkflow.xml') self.do_next_unique_task('Start') self.do_next_unique_task('first') - self.do_next_unique_task('sub_workflow_1') - # Inner: + + # Inner. The subworkflow task will complete automatically after the subwokflow completes self.do_next_unique_task('Start') self.do_next_unique_task('first') self.do_next_unique_task('last') self.do_next_unique_task('End') + # Back to outer: self.do_next_unique_task('last') self.do_next_unique_task('End') @@ -72,7 +75,7 @@ class TaskSpecTest(unittest.TestCase): self.load_workflow_spec('data', 'subworkflow_to_block.xml') self.do_next_unique_task('Start') self.do_next_unique_task('first') - self.do_next_unique_task('sub_workflow_1') + # Inner: self.do_next_unique_task('Start') self.do_next_unique_task('first') @@ -86,8 +89,9 @@ class TaskSpecTest(unittest.TestCase): self.load_workflow_spec('control-flow', 'subworkflow_to_join.xml') self.do_next_unique_task('Start') self.do_next_unique_task('first') - self.do_next_named_step('second', ['sub_workflow_1']) - self.do_next_unique_task('sub_workflow_1') + # The subworkflow task now sets its child tasks to READY and waits + self.do_next_named_step('second', ['Start']) + # Inner: self.do_next_unique_task('Start') self.do_next_unique_task('first') @@ -102,8 +106,8 @@ class TaskSpecTest(unittest.TestCase): self.load_workflow_spec('control-flow', 'subworkflow_to_join.xml') self.do_next_unique_task('Start') self.do_next_unique_task('first') - self.do_next_named_step('second', ['sub_workflow_1']) - self.do_next_unique_task('sub_workflow_1') + self.do_next_named_step('second', ['Start']) + # Inner: self.do_next_unique_task('Start') self.do_next_unique_task('first') diff --git a/tests/SpiffWorkflow/util.py b/tests/SpiffWorkflow/util.py index 5bc5a0aa..eeba7d52 100644 --- a/tests/SpiffWorkflow/util.py +++ b/tests/SpiffWorkflow/util.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from builtins import str -from builtins import range import time from SpiffWorkflow.task import Task, TaskState from SpiffWorkflow.workflow import Workflow @@ -33,7 +31,6 @@ def on_reached_cb(workflow, task, taken_path): props = [] for key, value in list(task.task_spec.data.items()): props.append('='.join((key, str(value)))) - # print "REACHED:", task.get_name(), atts, props # Store the list of data in the workflow. atts = ';'.join(atts) From f2b16112347ea193b717306e0ab4e1226cd66d16 Mon Sep 17 00:00:00 2001 From: burnettk Date: Fri, 18 Nov 2022 10:03:37 -0500 Subject: [PATCH 2/3] Squashed 'flask-bpmn/' changes from 5e40777f..860f2387 860f2387 Merge pull request #169 from sartography/dependabot/pip/flake8-rst-docstrings-0.3.0 2d0e83c8 Bump flake8-rst-docstrings from 0.2.7 to 0.3.0 6f6762ec Merge pull request #168 from sartography/dependabot/pip/mypy-0.991 579703c1 Bump mypy from 0.990 to 0.991 886bfdc3 Merge pull request #167 from sartography/dependabot/pip/flask-migrate-4.0.0 9b8b70d2 Merge pull request #165 from sartography/dependabot/pip/dot-github/workflows/virtualenv-20.16.7 9ec0402b Bump flask-migrate from 3.1.0 to 4.0.0 bc23eaa5 Bump virtualenv from 20.16.6 to 20.16.7 in /.github/workflows git-subtree-dir: flask-bpmn git-subtree-split: 860f2387bebdaa9220e9fbf6f8fa7f74e805d0d4 --- .github/workflows/constraints.txt | 2 +- poetry.lock | 87 ++++++++++++++++--------------- pyproject.toml | 4 +- 3 files changed, 48 insertions(+), 45 deletions(-) diff --git a/.github/workflows/constraints.txt b/.github/workflows/constraints.txt index a772cfa6..f30a60fc 100644 --- a/.github/workflows/constraints.txt +++ b/.github/workflows/constraints.txt @@ -2,4 +2,4 @@ pip==22.3.1 nox==2022.8.7 nox-poetry==1.0.2 poetry==1.2.2 -virtualenv==20.16.6 +virtualenv==20.16.7 diff --git a/poetry.lock b/poetry.lock index 7648bbd4..8b17963a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -491,17 +491,20 @@ flake8 = "*" [[package]] name = "flake8-rst-docstrings" -version = "0.2.7" -description = "Python docstring reStructuredText (RST) validator" +version = "0.3.0" +description = "Python docstring reStructuredText (RST) validator for flake8" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -flake8 = ">=3.0.0" +flake8 = ">=3" pygments = "*" restructuredtext-lint = "*" +[package.extras] +develop = ["build", "twine"] + [[package]] name = "flask" version = "2.2.2" @@ -595,7 +598,7 @@ tests = ["flask-sqlalchemy", "marshmallow-sqlalchemy (>=0.13.0)", "marshmallow-s [[package]] name = "flask-migrate" -version = "3.1.0" +version = "4.0.0" description = "SQLAlchemy database migrations for Flask applications using Alembic." category = "main" optional = false @@ -916,7 +919,7 @@ mypy-extensions = "*" [[package]] name = "mypy" -version = "0.990" +version = "0.991" description = "Optional static typing for Python" category = "dev" optional = false @@ -1501,7 +1504,7 @@ test = ["pytest"] [[package]] name = "SpiffWorkflow" version = "1.2.1" -description = "A workflow framework and BPMN/DMN Processor" +description = "" category = "main" optional = false python-versions = "*" @@ -1517,7 +1520,7 @@ lxml = "*" type = "git" url = "https://github.com/sartography/SpiffWorkflow" reference = "main" -resolved_reference = "580939cc8cb0b7ade1571483bd1e28f554434ac4" +resolved_reference = "025bc30f27366e06dd1286b7563e4b1cb04c1c46" [[package]] name = "sqlalchemy" @@ -1767,7 +1770,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "59fd1a96fd43ad04e57500a4a2f9c2a8b09279872f0277736d52a827634977a6" +content-hash = "6dfda037ebb3024834a45670108756a3057fff1b6fb5b916d222d3a162509b7d" [metadata.files] alabaster = [ @@ -2045,8 +2048,8 @@ flake8-polyfill = [ {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, ] flake8-rst-docstrings = [ - {file = "flake8-rst-docstrings-0.2.7.tar.gz", hash = "sha256:2740067ab9237559dd45a3434d8c987792c7b259ca563621a3b95efe201f5382"}, - {file = "flake8_rst_docstrings-0.2.7-py3-none-any.whl", hash = "sha256:5d56075dce360bcc9c6775bfe7cb431aa395de600ca7e8d40580a28d50b2a803"}, + {file = "flake8-rst-docstrings-0.3.0.tar.gz", hash = "sha256:d1ce22b4bd37b73cd86b8d980e946ef198cfcc18ed82fedb674ceaa2f8d1afa4"}, + {file = "flake8_rst_docstrings-0.3.0-py3-none-any.whl", hash = "sha256:f8c3c6892ff402292651c31983a38da082480ad3ba253743de52989bdc84ca1c"}, ] flask = [ {file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"}, @@ -2071,8 +2074,8 @@ flask-marshmallow = [ {file = "flask_marshmallow-0.14.0-py2.py3-none-any.whl", hash = "sha256:2adcd782b5a4a6c5ae3c96701f320d8ca6997995a52b2661093c56cc3ed24754"}, ] flask-migrate = [ - {file = "Flask-Migrate-3.1.0.tar.gz", hash = "sha256:57d6060839e3a7f150eaab6fe4e726d9e3e7cffe2150fb223d73f92421c6d1d9"}, - {file = "Flask_Migrate-3.1.0-py3-none-any.whl", hash = "sha256:a6498706241aba6be7a251078de9cf166d74307bca41a4ca3e403c9d39e2f897"}, + {file = "Flask-Migrate-4.0.0.tar.gz", hash = "sha256:2a301c3040af6844f29d9149abe428a0f08ebc8fa149e72113bbb36fa341920a"}, + {file = "Flask_Migrate-4.0.0-py3-none-any.whl", hash = "sha256:e75a46b657e3062296b9f6e92f31e19662f76cfee8abd6ae94640cbcb79fe016"}, ] flask-restful = [ {file = "Flask-RESTful-0.3.9.tar.gz", hash = "sha256:ccec650b835d48192138c85329ae03735e6ced58e9b2d9c2146d6c84c06fa53e"}, @@ -2391,36 +2394,36 @@ monkeytype = [ {file = "MonkeyType-22.2.0.tar.gz", hash = "sha256:6b0c00b49dcc5095a2c08d28246cf005e05673fc51f64d203f9a6bca2036dfab"}, ] mypy = [ - {file = "mypy-0.990-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aaf1be63e0207d7d17be942dcf9a6b641745581fe6c64df9a38deb562a7dbafa"}, - {file = "mypy-0.990-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d555aa7f44cecb7ea3c0ac69d58b1a5afb92caa017285a8e9c4efbf0518b61b4"}, - {file = "mypy-0.990-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f694d6d09a460b117dccb6857dda269188e3437c880d7b60fa0014fa872d1e9"}, - {file = "mypy-0.990-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:269f0dfb6463b8780333310ff4b5134425157ef0d2b1d614015adaf6d6a7eabd"}, - {file = "mypy-0.990-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8798c8ed83aa809f053abff08664bdca056038f5a02af3660de00b7290b64c47"}, - {file = "mypy-0.990-cp310-cp310-win_amd64.whl", hash = "sha256:47a9955214615108c3480a500cfda8513a0b1cd3c09a1ed42764ca0dd7b931dd"}, - {file = "mypy-0.990-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4a8a6c10f4c63fbf6ad6c03eba22c9331b3946a4cec97f008e9ffb4d3b31e8e2"}, - {file = "mypy-0.990-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cd2dd3730ba894ec2a2082cc703fbf3e95a08479f7be84912e3131fc68809d46"}, - {file = "mypy-0.990-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7da0005e47975287a92b43276e460ac1831af3d23032c34e67d003388a0ce8d0"}, - {file = "mypy-0.990-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:262c543ef24deb10470a3c1c254bb986714e2b6b1a67d66daf836a548a9f316c"}, - {file = "mypy-0.990-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3ff201a0c6d3ea029d73b1648943387d75aa052491365b101f6edd5570d018ea"}, - {file = "mypy-0.990-cp311-cp311-win_amd64.whl", hash = "sha256:1767830da2d1afa4e62b684647af0ff79b401f004d7fa08bc5b0ce2d45bcd5ec"}, - {file = "mypy-0.990-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6826d9c4d85bbf6d68cb279b561de6a4d8d778ca8e9ab2d00ee768ab501a9852"}, - {file = "mypy-0.990-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46897755f944176fbc504178422a5a2875bbf3f7436727374724842c0987b5af"}, - {file = "mypy-0.990-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0680389c34284287fe00e82fc8bccdea9aff318f7e7d55b90d967a13a9606013"}, - {file = "mypy-0.990-cp37-cp37m-win_amd64.whl", hash = "sha256:b08541a06eed35b543ae1a6b301590eb61826a1eb099417676ddc5a42aa151c5"}, - {file = "mypy-0.990-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:be88d665e76b452c26fb2bdc3d54555c01226fba062b004ede780b190a50f9db"}, - {file = "mypy-0.990-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b8f4a8213b1fd4b751e26b59ae0e0c12896568d7e805861035c7a15ed6dc9eb"}, - {file = "mypy-0.990-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b6f85c2ad378e3224e017904a051b26660087b3b76490d533b7344f1546d3ff"}, - {file = "mypy-0.990-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee5f99817ee70254e7eb5cf97c1b11dda29c6893d846c8b07bce449184e9466"}, - {file = "mypy-0.990-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49082382f571c3186ce9ea0bd627cb1345d4da8d44a8377870f4442401f0a706"}, - {file = "mypy-0.990-cp38-cp38-win_amd64.whl", hash = "sha256:aba38e3dd66bdbafbbfe9c6e79637841928ea4c79b32e334099463c17b0d90ef"}, - {file = "mypy-0.990-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9d851c09b981a65d9d283a8ccb5b1d0b698e580493416a10942ef1a04b19fd37"}, - {file = "mypy-0.990-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d847dd23540e2912d9667602271e5ebf25e5788e7da46da5ffd98e7872616e8e"}, - {file = "mypy-0.990-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc6019808580565040cd2a561b593d7c3c646badd7e580e07d875eb1bf35c695"}, - {file = "mypy-0.990-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a3150d409609a775c8cb65dbe305c4edd7fe576c22ea79d77d1454acd9aeda8"}, - {file = "mypy-0.990-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3227f14fe943524f5794679156488f18bf8d34bfecd4623cf76bc55958d229c5"}, - {file = "mypy-0.990-cp39-cp39-win_amd64.whl", hash = "sha256:c76c769c46a1e6062a84837badcb2a7b0cdb153d68601a61f60739c37d41cc74"}, - {file = "mypy-0.990-py3-none-any.whl", hash = "sha256:8f1940325a8ed460ba03d19ab83742260fa9534804c317224e5d4e5aa588e2d6"}, - {file = "mypy-0.990.tar.gz", hash = "sha256:72382cb609142dba3f04140d016c94b4092bc7b4d98ca718740dc989e5271b8d"}, + {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, + {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, + {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"}, + {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"}, + {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"}, + {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"}, + {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"}, + {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"}, + {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"}, + {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"}, + {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"}, + {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"}, + {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"}, + {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"}, + {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"}, + {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"}, + {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"}, + {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"}, + {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"}, + {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"}, + {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"}, + {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"}, + {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"}, + {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"}, + {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"}, + {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"}, + {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"}, + {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"}, + {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"}, + {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, diff --git a/pyproject.toml b/pyproject.toml index c13cd007..3cb3217a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ greenlet = "^2.0.1" pytest = "^6.2.5" coverage = {extras = ["toml"], version = "^6.5"} safety = "^2.3.1" -mypy = "^0.990" +mypy = "^0.991" typeguard = "^2.13.2" xdoctest = {extras = ["colors"], version = "^1.1.0"} sphinx = "^4.3.0" @@ -55,7 +55,7 @@ bandit = "1.7.2" flake8-bugbear = "^22.10.27" flake8-docstrings = "^1.6.0" -flake8-rst-docstrings = "^0.2.7" +flake8-rst-docstrings = "^0.3.0" pep8-naming = "^0.13.2" darglint = "^1.8.1" reorder-python-imports = "^3.9.0" From c574ceba766b56be43633c24694294363a4da4f2 Mon Sep 17 00:00:00 2001 From: burnettk Date: Fri, 18 Nov 2022 10:03:38 -0500 Subject: [PATCH 3/3] Squashed 'bpmn-js-spiffworkflow/' changes from 4f23f860..e92f48da e92f48da minor fixes to the Bpmn-js-subworkflow spec. af88025a Merge remote-tracking branch 'origin/main' into feature/call_activity_selection f3bd1e56 Minor tweak to bpmn-js-spiffworkflow 9207ef45 Minor tweak, in the hopes of getting a text box to update correctly. 084f4646 Fixing a bug in the Call Activity's handing of an update. c9035994 Return the event bus when calling a search. git-subtree-dir: bpmn-js-spiffworkflow git-subtree-split: e92f48da7cb4416310af71bb1699caaca87324cd --- README.md | 49 ++++++++++--------- app/app.js | 11 +++++ .../CallActivityPropertiesProvider.js | 23 ++++++--- .../propertiesPanel/SpiffExtensionSelect.js | 5 +- test/spec/CallActivitySpec.js | 3 +- 5 files changed, 58 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 43c3eb8e..de473b9f 100644 --- a/README.md +++ b/README.md @@ -71,30 +71,31 @@ the behavior of the editor making it easier for your end users. There are many this in the app.js file. Below is a table of all the events that are sent and accepted: -| Event Name | Description | Fired or Acceped | Parameters | Description | -|--------------------------------|------------------------------------------------------------------------------| ---------------- |----------------------|--------------------------------------------------------------------------| -| spiff.service\_tasks.requested | Request a list of available services that can be called from a service task. | Fired | \- | | -| spiff.service\_tasks.returned | Provides a list of services. | Recieved | serviceTaskOperators | ex: \[{id:'Chuck Facts', parameters\[{id:'category', type:'string'}\]}\] | -| spiff.script.edit | Request to edit a python script in some sort of facy editor. | Fired | scriptType | one of: script, preScript, postScript | -| | | | value | The actual python script | -| | | | element | The element that needs updating | -| | | | eventBus | Used by receiver to fire back an event | -| spiff.script.update | Update a python script to a new value. | Recieved | scriptType | one of: script, preScript, postScript | -| | | | value | The updated script | -| | | | element | The element that needs updating | -| spiff.markdown.edit | Request to edit markdown in a fancy editor. | Fired | element | The element that needs updating | -| | | | value | The current markdown content | -| spiff.markdown.update | Update Markdown content for a paticular elements 'instructions'. | Recieved | element | The element that needs updating | -| | | | value | Tne updated Markdown content | -| spiff.callactivity.edit | Requst to edit a call activity by process id. | Fired | processId | The Process the users wants to edit | -| spiff.callactivity.search | Requst to search for a call activity | Fired | | | -| spiff.callactivity.update | Update the process id from a call activity (based on search) | Fired | processId | The Process the users wants to edit | -| spiff.file.edit | request to edit a file, but file name. | Fired | value | The file name the user wants to edit | -| spiff.dmn.edit | request to edit a dmn by process id. | Fired | value | The DMN id the user wants to edit | -| spiff.json\_files.requested | request a list of local json files. | Fired | optionType | The type of options required ('json' or 'dmn') | -| spff.dmn\_files.requested | request of list of local dmn files. | | | | -| spiff.json\_files.returned | Return a list of available json files | Recieved | options | \[{lable:'My Label', value:'1'}\] | -| spff.dmn\_files.returned | Return a list of available dmn files. | Recieved | options | \[{lable:'My Label', value:'1'}\] | +| Event Name | Description | Fired or Acceped | Parameters | Description | +|--------------------------------|------------------------------------------------------------------|---------|----------------------|--------------------------------------------------------------------------| +| spiff.service\_tasks.requested | Request a list of available services for service task. | Fired | \- | | +| spiff.service\_tasks.returned | Provides a list of services. | Recieved | serviceTaskOperators | ex: \[{id:'Chuck Facts', parameters\[{id:'category', type:'string'}\]}\] | +| spiff.script.edit | Request to edit a python script in some sort of facy editor. | Fired | scriptType | one of: script, preScript, postScript | +| | | | value | The actual python script | +| | | | element | The element that needs updating | +| | | | eventBus | Used by receiver to fire back an event | +| spiff.script.update | Update a python script to a new value. | Recieved | scriptType | one of: script, preScript, postScript | +| | | | value | The updated script | +| | | | element | The element that needs updating | +| spiff.markdown.edit | Request to edit markdown in a fancy editor. | Fired | element | The element that needs updating | +| | | | value | The current markdown content | +| spiff.markdown.update | Update Markdown content for a paticular elements 'instructions'. | Recieved | element | The element that needs updating | +| | | | value | Tne updated Markdown content | +| spiff.callactivity.edit | Requst to edit a call activity by process id. | Fired | processId | The Process the users wants to edit | +| spiff.callactivity.search | Requst to search for a call activity | Fired | processUd | The currently seleted process id | +| | | | eventBus | For sending back the selected process id. | +| spiff.callactivity.update | Update the process id from a call activity (based on search) | Received | processId | The Process the users wants to edit | +| spiff.file.edit | request to edit a file, but file name. | Fired | value | The file name the user wants to edit | +| spiff.dmn.edit | request to edit a dmn by process id. | Fired | value | The DMN id the user wants to edit | +| spiff.json\_files.requested | request a list of local json files. | Fired | optionType | The type of options required ('json' or 'dmn') | +| spff.dmn\_files.requested | request of list of local dmn files. | | | | +| spiff.json\_files.returned | Return a list of available json files | Recieved | options | \[{lable:'My Label', value:'1'}\] | +| spff.dmn\_files.returned | Return a list of available dmn files. | Recieved | options | \[{lable:'My Label', value:'1'}\] | diff --git a/app/app.js b/app/app.js index 00eac02e..3876df3d 100644 --- a/app/app.js +++ b/app/app.js @@ -181,6 +181,17 @@ bpmnModeler.on('spiff.dmn_files.requested', (event) => { }); }); +// As call activites might refernce processes across the system +// it should be possible to search for a paticular call activity. +bpmnModeler.on('spiff.callactivity.search', (event) => { + console.log("Firing call activity update", event.element) + event.eventBus.fire('spiff.callactivity.update', { + value: 'searched_bpmn_id', + element: event.element, + }); +}); + + // This handles the download and upload buttons - it isn't specific to // the BPMN modeler or these extensions, just a quick way to allow you to // create and save files, so keeping it outside the example. diff --git a/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js b/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js index c5d20851..8aee91db 100644 --- a/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js +++ b/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js @@ -98,20 +98,29 @@ function CalledElementTextField(props) { } function FindProcessButton(props) { - const { element } = props; + const { element, commandStack } = props; const eventBus = useService('eventBus'); return HeaderButton({ id: 'spiffworkflow-search-call-activity-button', class: 'spiffworkflow-properties-panel-button', onClick: () => { const processId = getCalledElementValue(element); - eventBus.fire('spiff.callactivity.search', { - element, - processId, - }); - // Listen for a response, to update the script. + + // First, set up the listen, then fire the event, just + // in case we are testing and things are happening superfast. eventBus.once('spiff.callactivity.update', (response) => { - element.businessObject.calledElement = response.value; + commandStack.execute('element.updateProperties', { + element: response.element, + moddleElement: response.element.businessObject, + properties: { + calledElement: response.value, + }, + }); + }); + eventBus.fire('spiff.callactivity.search', { + processId, + eventBus, + element }); }, children: 'Search', diff --git a/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js b/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js index 5bc6f505..9f7bbbaa 100644 --- a/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js +++ b/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js @@ -53,7 +53,10 @@ export function SpiffExtensionSelect(props) { } const getOptions = () => { const optionList = []; - if (optionType in spiffExtensionOptions) { + if ( + optionType in spiffExtensionOptions && + spiffExtensionOptions[optionType] !== null + ) { spiffExtensionOptions[optionType].forEach((opt) => { optionList.push({ label: opt.label, diff --git a/test/spec/CallActivitySpec.js b/test/spec/CallActivitySpec.js index b2110bb0..56c52cee 100644 --- a/test/spec/CallActivitySpec.js +++ b/test/spec/CallActivitySpec.js @@ -102,8 +102,9 @@ describe('Call Activities should work', function () { }); await pressButton(button); expect(launchEvent.processId).to.exist; + expect(launchEvent.element).to.exist; - eventBus.fire('spiff.callactivity.update', {value: 'searchedProcessId'}); + eventBus.fire('spiff.callactivity.update', {value: 'searchedProcessId', element: launchEvent.element}); const textInput = domQuery('input', entry); expect(businessObject.get('calledElement')).to.equal('searchedProcessId');