Improvement/flexible task iteration (#507)

* updated to use new spiff branch and fixed broken tests w/ burnettk essweine

* updated spiffworkflow with new main build w/ burnettk essweine

* initial updates to new spiff branch w/ burnettk essweine

* more updates for new spiff w/ burnettk essweine

* fixed some linting issues w/ burnettk essweine

* fixed some failing tests w/ burnettk

* updated spiffworkflow for cancel fix w/ burnettk

* Improvement/flexible task iteration 2 (#504)

* wip

* consistent failure, mostly

* removing test code and tests

* removing unused test bpmn files

* removing unused test bpmn files

* minor cleanup of commented code

* spaces and unused imports

* go back to spiff on main

---------

Co-authored-by: burnettk <burnettk@users.noreply.github.com>

* lint

* updated test to reflect storing predicted tasks w/ burnettk

* add some orders so postgres does not do whatever it wants, and clear log

* fix lint

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
Co-authored-by: danfunk <daniel.h.funk@gmail.com>
Co-authored-by: burnettk <burnettk@users.noreply.github.com>
This commit is contained in:
jasquat 2023-09-21 17:47:18 -04:00 committed by GitHub
parent c986dbb9e5
commit 1826cc4b6c
16 changed files with 76 additions and 65 deletions

View File

@ -70,11 +70,19 @@ if [[ "$subcommand" != "pre" ]] || [[ -n "$(git status --porcelain "spiffworkflo
run_pre_commmit || run_pre_commmit run_pre_commmit || run_pre_commmit
fi fi
function clear_log_file() {
unit_testing_log_file="./log/unit_testing.log"
if [[ -f "$unit_testing_log_file" ]]; then
> "$unit_testing_log_file"
fi
}
for python_project in "${python_projects[@]}"; do for python_project in "${python_projects[@]}"; do
if [[ "$subcommand" != "pre" ]] || [[ -n "$(git status --porcelain "$python_project")" ]]; then if [[ "$subcommand" != "pre" ]] || [[ -n "$(git status --porcelain "$python_project")" ]]; then
pushd "$python_project" pushd "$python_project"
poetry install poetry install
poetry run mypy $(get_python_dirs) poetry run mypy $(get_python_dirs)
clear_log_file
./bin/tests-par ./bin/tests-par
popd popd
fi fi

View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
[[package]] [[package]]
name = "alembic" name = "alembic"
@ -2366,7 +2366,7 @@ lxml = "*"
type = "git" type = "git"
url = "https://github.com/sartography/SpiffWorkflow" url = "https://github.com/sartography/SpiffWorkflow"
reference = "main" reference = "main"
resolved_reference = "f113aba0f59f18e9dce7263289c441a28a83b4c6" resolved_reference = "90159bd23c3c74fcf480414473e851460a01e92c"
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
@ -2419,7 +2419,7 @@ files = [
] ]
[package.dependencies] [package.dependencies]
greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
typing-extensions = ">=4.2.0" typing-extensions = ">=4.2.0"
[package.extras] [package.extras]

View File

@ -8,7 +8,7 @@ from typing import Any
import marshmallow import marshmallow
from marshmallow import Schema from marshmallow import Schema
from marshmallow_enum import EnumField # type: ignore from marshmallow_enum import EnumField # type: ignore
from SpiffWorkflow.task import TaskStateNames # type: ignore from SpiffWorkflow.util.task import TaskState # type: ignore
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@ -232,9 +232,8 @@ class Task:
@classmethod @classmethod
def task_state_name_to_int(cls, task_state_name: str) -> int: def task_state_name_to_int(cls, task_state_name: str) -> int:
task_state_integers = {v: k for k, v in TaskStateNames.items()} state_value: int = TaskState.get_value(task_state_name)
task_state_int: int = task_state_integers[task_state_name] return state_value
return task_state_int
class OptionSchema(Schema): class OptionSchema(Schema):

View File

@ -18,7 +18,7 @@ from MySQLdb import OperationalError # type: ignore
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.task import TaskState from SpiffWorkflow.util.task import TaskState # type: ignore
from sqlalchemy import and_ from sqlalchemy import and_
from sqlalchemy import asc from sqlalchemy import asc
from sqlalchemy import desc from sqlalchemy import desc
@ -493,7 +493,7 @@ def _interstitial_stream(
) -> Generator[str, str | None, None]: ) -> Generator[str, str | None, None]:
def get_reportable_tasks() -> Any: def get_reportable_tasks() -> Any:
return processor.bpmn_process_instance.get_tasks( return processor.bpmn_process_instance.get_tasks(
TaskState.WAITING | TaskState.STARTED | TaskState.READY | TaskState.ERROR state=TaskState.WAITING | TaskState.STARTED | TaskState.READY | TaskState.ERROR
) )
def render_instructions(spiff_task: SpiffTask) -> str: def render_instructions(spiff_task: SpiffTask) -> str:
@ -607,7 +607,7 @@ def _interstitial_stream(
def _get_ready_engine_step_count(bpmn_process_instance: BpmnWorkflow) -> int: def _get_ready_engine_step_count(bpmn_process_instance: BpmnWorkflow) -> int:
return len([t for t in bpmn_process_instance.get_tasks(TaskState.READY) if not t.task_spec.manual]) return len([t for t in bpmn_process_instance.get_tasks(state=TaskState.READY) if not t.task_spec.manual])
def _dequeued_interstitial_stream( def _dequeued_interstitial_stream(

View File

@ -41,8 +41,9 @@ from SpiffWorkflow.serializer.exceptions import MissingSpecError # type: ignore
from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser # type: ignore from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser # type: ignore
from SpiffWorkflow.spiff.serializer.config import SPIFF_SPEC_CONFIG # type: ignore from SpiffWorkflow.spiff.serializer.config import SPIFF_SPEC_CONFIG # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.task import TaskState
from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
from SpiffWorkflow.util.task import TaskIterator # type: ignore
from SpiffWorkflow.util.task import TaskState
from spiffworkflow_backend.data_stores.json import JSONDataStore from spiffworkflow_backend.data_stores.json import JSONDataStore
from spiffworkflow_backend.data_stores.json import JSONFileDataStore from spiffworkflow_backend.data_stores.json import JSONFileDataStore
from spiffworkflow_backend.data_stores.typeahead import TypeaheadDataStore from spiffworkflow_backend.data_stores.typeahead import TypeaheadDataStore
@ -790,8 +791,6 @@ class ProcessInstanceProcessor:
) )
bpmn_process_instance.data[ProcessInstanceProcessor.VALIDATION_PROCESS_KEY] = validate_only bpmn_process_instance.data[ProcessInstanceProcessor.VALIDATION_PROCESS_KEY] = validate_only
# run _predict to ensure tasks are predicted to add back in LIKELY and MAYBE tasks
bpmn_process_instance._predict()
return ( return (
bpmn_process_instance, bpmn_process_instance,
full_bpmn_process_dict, full_bpmn_process_dict,
@ -1044,16 +1043,6 @@ class ProcessInstanceProcessor:
db.session.add(self.process_instance_model) db.session.add(self.process_instance_model)
db.session.commit() db.session.commit()
known_task_ids = [str(t.id) for t in self.bpmn_process_instance.get_tasks()]
TaskModel.query.filter(TaskModel.process_instance_id == self.process_instance_model.id).filter(
TaskModel.guid.notin_(known_task_ids) # type: ignore
).delete()
HumanTaskModel.query.filter(HumanTaskModel.process_instance_id == self.process_instance_model.id).filter(
HumanTaskModel.task_id.notin_(known_task_ids) # type: ignore
).delete()
db.session.commit()
human_tasks = HumanTaskModel.query.filter_by( human_tasks = HumanTaskModel.query.filter_by(
process_instance_id=self.process_instance_model.id, completed=False process_instance_id=self.process_instance_model.id, completed=False
).all() ).all()
@ -1111,7 +1100,7 @@ class ProcessInstanceProcessor:
task_name=ready_or_waiting_task.task_spec.bpmn_id, task_name=ready_or_waiting_task.task_spec.bpmn_id,
task_title=ready_or_waiting_task.task_spec.bpmn_name, task_title=ready_or_waiting_task.task_spec.bpmn_name,
task_type=ready_or_waiting_task.task_spec.__class__.__name__, task_type=ready_or_waiting_task.task_spec.__class__.__name__,
task_status=ready_or_waiting_task.get_state_name(), task_status=TaskState.get_name(ready_or_waiting_task.state),
lane_assignment_id=potential_owner_hash["lane_assignment_id"], lane_assignment_id=potential_owner_hash["lane_assignment_id"],
) )
db.session.add(human_task) db.session.add(human_task)
@ -1365,14 +1354,14 @@ class ProcessInstanceProcessor:
def status_of(bpmn_process_instance: BpmnWorkflow) -> ProcessInstanceStatus: def status_of(bpmn_process_instance: BpmnWorkflow) -> ProcessInstanceStatus:
if bpmn_process_instance.is_completed(): if bpmn_process_instance.is_completed():
return ProcessInstanceStatus.complete return ProcessInstanceStatus.complete
user_tasks = bpmn_process_instance.get_ready_user_tasks() user_tasks = bpmn_process_instance.get_tasks(state=TaskState.READY, manual=True)
# workflow.waiting_events (includes timers, and timers have a when firing property) # workflow.waiting_events (includes timers, and timers have a when firing property)
# if the process instance has status "waiting" it will get picked up # if the process instance has status "waiting" it will get picked up
# by background processing. when that happens it can potentially overwrite # by background processing. when that happens it can potentially overwrite
# human tasks which is bad because we cache them with the previous id's. # human tasks which is bad because we cache them with the previous id's.
# waiting_tasks = bpmn_process_instance.get_tasks(TaskState.WAITING) # waiting_tasks = bpmn_process_instance.get_tasks(state=TaskState.WAITING)
# waiting_tasks = bpmn_process_instance.get_waiting() # waiting_tasks = bpmn_process_instance.get_waiting()
# if len(waiting_tasks) > 0: # if len(waiting_tasks) > 0:
# return ProcessInstanceStatus.waiting # return ProcessInstanceStatus.waiting
@ -1409,7 +1398,7 @@ class ProcessInstanceProcessor:
return None return None
def lazy_load_subprocess_specs(self) -> None: def lazy_load_subprocess_specs(self) -> None:
tasks = self.bpmn_process_instance.get_tasks(TaskState.DEFINITE_MASK) tasks = self.bpmn_process_instance.get_tasks(state=TaskState.DEFINITE_MASK)
loaded_specs = set(self.bpmn_process_instance.subprocess_specs.keys()) loaded_specs = set(self.bpmn_process_instance.subprocess_specs.keys())
for task in tasks: for task in tasks:
if task.task_spec.description != "Call Activity": if task.task_spec.description != "Call Activity":
@ -1489,7 +1478,7 @@ class ProcessInstanceProcessor:
@classmethod @classmethod
def get_tasks_with_data(cls, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]: def get_tasks_with_data(cls, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]:
return [task for task in bpmn_process_instance.get_tasks(TaskState.FINISHED_MASK) if len(task.data) > 0] return [task for task in bpmn_process_instance.get_tasks(state=TaskState.FINISHED_MASK) if len(task.data) > 0]
@classmethod @classmethod
def get_task_data_size(cls, bpmn_process_instance: BpmnWorkflow) -> int: def get_task_data_size(cls, bpmn_process_instance: BpmnWorkflow) -> int:
@ -1531,7 +1520,7 @@ class ProcessInstanceProcessor:
return self._serializer.workflow_to_dict(self.bpmn_process_instance) # type: ignore return self._serializer.workflow_to_dict(self.bpmn_process_instance) # type: ignore
def next_user_tasks(self) -> list[SpiffTask]: def next_user_tasks(self) -> list[SpiffTask]:
return self.bpmn_process_instance.get_ready_user_tasks() # type: ignore return self.bpmn_process_instance.get_tasks(state=TaskState.READY, manual=True) # type: ignore
def next_task(self) -> SpiffTask: def next_task(self) -> SpiffTask:
"""Returns the next task that should be completed even if there are parallel tasks and multiple options are available. """Returns the next task that should be completed even if there are parallel tasks and multiple options are available.
@ -1545,7 +1534,7 @@ class ProcessInstanceProcessor:
endtasks = [] endtasks = []
if self.bpmn_process_instance.is_completed(): if self.bpmn_process_instance.is_completed():
for spiff_task in SpiffTask.Iterator(self.bpmn_process_instance.task_tree, TaskState.ANY_MASK): for spiff_task in TaskIterator(self.bpmn_process_instance.task_tree, TaskState.ANY_MASK):
# Assure that we find the end event for this process_instance, and not for any sub-process_instances. # Assure that we find the end event for this process_instance, and not for any sub-process_instances.
if TaskService.is_main_process_end_event(spiff_task): if TaskService.is_main_process_end_event(spiff_task):
endtasks.append(spiff_task) endtasks.append(spiff_task)
@ -1557,17 +1546,17 @@ class ProcessInstanceProcessor:
# a parallel gateway with multiple tasks, so prefer ones that share a parent. # a parallel gateway with multiple tasks, so prefer ones that share a parent.
# Get a list of all ready tasks # Get a list of all ready tasks
ready_tasks = self.bpmn_process_instance.get_tasks(TaskState.READY) ready_tasks = self.bpmn_process_instance.get_tasks(state=TaskState.READY)
if len(ready_tasks) == 0: if len(ready_tasks) == 0:
# If no ready tasks exist, check for a waiting task. # If no ready tasks exist, check for a waiting task.
waiting_tasks = self.bpmn_process_instance.get_tasks(TaskState.WAITING) waiting_tasks = self.bpmn_process_instance.get_tasks(state=TaskState.WAITING)
if len(waiting_tasks) > 0: if len(waiting_tasks) > 0:
return waiting_tasks[0] return waiting_tasks[0]
# If there are no ready tasks, and not waiting tasks, return the latest error. # If there are no ready tasks, and not waiting tasks, return the latest error.
error_task = None error_task = None
for task in SpiffTask.Iterator(self.bpmn_process_instance.task_tree, TaskState.ERROR): for task in TaskIterator(self.bpmn_process_instance.task_tree, TaskState.ERROR):
error_task = task error_task = task
return error_task return error_task
@ -1582,7 +1571,7 @@ class ProcessInstanceProcessor:
last_user_task = completed_user_tasks[0] last_user_task = completed_user_tasks[0]
if len(ready_tasks) > 0: if len(ready_tasks) > 0:
for task in ready_tasks: for task in ready_tasks:
if task._is_descendant_of(last_user_task): if task.is_descendant_of(last_user_task):
return task return task
for task in ready_tasks: for task in ready_tasks:
if self.bpmn_process_instance.last_task and task.parent == last_user_task.parent: if self.bpmn_process_instance.last_task and task.parent == last_user_task.parent:
@ -1593,12 +1582,12 @@ class ProcessInstanceProcessor:
# If there are no ready tasks, but the thing isn't complete yet, find the first non-complete task # If there are no ready tasks, but the thing isn't complete yet, find the first non-complete task
# and return that # and return that
next_task_to_return = None next_task_to_return = None
for task in SpiffTask.Iterator(self.bpmn_process_instance.task_tree, TaskState.NOT_FINISHED_MASK): for task in TaskIterator(self.bpmn_process_instance.task_tree, TaskState.NOT_FINISHED_MASK):
next_task_to_return = task next_task_to_return = task
return next_task_to_return return next_task_to_return
def completed_user_tasks(self) -> list[SpiffTask]: def completed_user_tasks(self) -> list[SpiffTask]:
user_tasks = self.bpmn_process_instance.get_tasks(TaskState.COMPLETED) user_tasks = self.bpmn_process_instance.get_tasks(state=TaskState.COMPLETED)
user_tasks.reverse() user_tasks.reverse()
user_tasks = list( user_tasks = list(
filter( filter(
@ -1638,7 +1627,7 @@ class ProcessInstanceProcessor:
human_task.completed_by_user_id = user.id human_task.completed_by_user_id = user.id
human_task.completed = True human_task.completed = True
human_task.task_status = spiff_task.get_state_name() human_task.task_status = TaskState.get_name(spiff_task.state)
db.session.add(human_task) db.session.add(human_task)
task_service = TaskService( task_service = TaskService(
@ -1701,11 +1690,11 @@ class ProcessInstanceProcessor:
return self.process_instance_model.id return self.process_instance_model.id
def get_ready_user_tasks(self) -> list[SpiffTask]: def get_ready_user_tasks(self) -> list[SpiffTask]:
return self.bpmn_process_instance.get_ready_user_tasks() # type: ignore return self.bpmn_process_instance.get_tasks(state=TaskState.READY, manual=True) # type: ignore
def get_current_user_tasks(self) -> list[SpiffTask]: def get_current_user_tasks(self) -> list[SpiffTask]:
"""Return a list of all user tasks that are READY or COMPLETE and are parallel to the READY Task.""" """Return a list of all user tasks that are READY or COMPLETE and are parallel to the READY Task."""
ready_tasks = self.bpmn_process_instance.get_ready_user_tasks() ready_tasks = self.bpmn_process_instance.get_tasks(state=TaskState.READY, manual=True)
additional_tasks = [] additional_tasks = []
if len(ready_tasks) > 0: if len(ready_tasks) > 0:
for child in ready_tasks[0].parent.children: for child in ready_tasks[0].parent.children:
@ -1714,19 +1703,19 @@ class ProcessInstanceProcessor:
return ready_tasks + additional_tasks # type: ignore return ready_tasks + additional_tasks # type: ignore
def get_all_user_tasks(self) -> list[SpiffTask]: def get_all_user_tasks(self) -> list[SpiffTask]:
all_tasks = self.bpmn_process_instance.get_tasks(TaskState.ANY_MASK) all_tasks = self.bpmn_process_instance.get_tasks(state=TaskState.ANY_MASK)
return [t for t in all_tasks if t.task_spec.manual] return [t for t in all_tasks if t.task_spec.manual]
def get_all_completed_tasks(self) -> list[SpiffTask]: def get_all_completed_tasks(self) -> list[SpiffTask]:
all_tasks = self.bpmn_process_instance.get_tasks(TaskState.ANY_MASK) all_tasks = self.bpmn_process_instance.get_tasks(state=TaskState.ANY_MASK)
return [t for t in all_tasks if t.task_spec.manual and t.state in [TaskState.COMPLETED, TaskState.CANCELLED]] return [t for t in all_tasks if t.task_spec.manual and t.state in [TaskState.COMPLETED, TaskState.CANCELLED]]
def get_all_waiting_tasks(self) -> list[SpiffTask]: def get_all_waiting_tasks(self) -> list[SpiffTask]:
all_tasks = self.bpmn_process_instance.get_tasks(TaskState.ANY_MASK) all_tasks = self.bpmn_process_instance.get_tasks(state=TaskState.ANY_MASK)
return [t for t in all_tasks if t.state in [TaskState.WAITING]] return [t for t in all_tasks if t.state in [TaskState.WAITING]]
def get_all_ready_or_waiting_tasks(self) -> list[SpiffTask]: def get_all_ready_or_waiting_tasks(self) -> list[SpiffTask]:
all_tasks = self.bpmn_process_instance.get_tasks(TaskState.ANY_MASK) all_tasks = self.bpmn_process_instance.get_tasks(state=TaskState.ANY_MASK)
return [t for t in all_tasks if t.state in [TaskState.WAITING, TaskState.READY]] return [t for t in all_tasks if t.state in [TaskState.WAITING, TaskState.READY]]
def get_task_by_guid(self, task_guid: str) -> SpiffTask | None: def get_task_by_guid(self, task_guid: str) -> SpiffTask | None:
@ -1736,7 +1725,7 @@ class ProcessInstanceProcessor:
def get_task_by_bpmn_identifier( def get_task_by_bpmn_identifier(
cls, bpmn_task_identifier: str, bpmn_process_instance: BpmnWorkflow cls, bpmn_task_identifier: str, bpmn_process_instance: BpmnWorkflow
) -> SpiffTask | None: ) -> SpiffTask | None:
all_tasks = bpmn_process_instance.get_tasks(TaskState.ANY_MASK) all_tasks = bpmn_process_instance.get_tasks(state=TaskState.ANY_MASK)
for task in all_tasks: for task in all_tasks:
if task.task_spec.name == bpmn_task_identifier: if task.task_spec.name == bpmn_task_identifier:
return task return task

View File

@ -14,6 +14,7 @@ from SpiffWorkflow.bpmn.event import PendingBpmnEvent # type: ignore
from SpiffWorkflow.bpmn.specs.control import BoundaryEventSplit # type: ignore from SpiffWorkflow.bpmn.specs.control import BoundaryEventSplit # type: ignore
from SpiffWorkflow.bpmn.specs.event_definitions.timer import TimerEventDefinition # type: ignore from SpiffWorkflow.bpmn.specs.event_definitions.timer import TimerEventDefinition # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend import db from spiffworkflow_backend import db
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
@ -199,7 +200,7 @@ class ProcessInstanceService:
@classmethod @classmethod
def ready_user_task_has_associated_timer(cls, processor: ProcessInstanceProcessor) -> bool: def ready_user_task_has_associated_timer(cls, processor: ProcessInstanceProcessor) -> bool:
for ready_user_task in processor.bpmn_process_instance.get_ready_user_tasks(): for ready_user_task in processor.bpmn_process_instance.get_tasks(state=TaskState.READY, manual=True):
if isinstance(ready_user_task.parent.task_spec, BoundaryEventSplit): if isinstance(ready_user_task.parent.task_spec, BoundaryEventSplit):
return True return True
return False return False
@ -596,7 +597,7 @@ class ProcessInstanceService:
spiff_task.task_spec.bpmn_id, spiff_task.task_spec.bpmn_id,
spiff_task.task_spec.bpmn_name, spiff_task.task_spec.bpmn_name,
task_type, task_type,
spiff_task.get_state_name(), TaskState.get_name(spiff_task.state),
can_complete=can_complete, can_complete=can_complete,
lane=lane, lane=lane,
process_identifier=spiff_task.task_spec._wf_spec.name, process_identifier=spiff_task.task_spec._wf_spec.name,

View File

@ -10,7 +10,7 @@ from lxml import etree # type: ignore
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.task import TaskState from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend.services.custom_parser import MyCustomParser from spiffworkflow_backend.services.custom_parser import MyCustomParser
@ -126,7 +126,7 @@ class ProcessModelTestRunnerMostlyPureSpiffDelegate(ProcessModelTestRunnerDelega
spiff_task.run() spiff_task.run()
def get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> SpiffTask | None: def get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> SpiffTask | None:
ready_tasks = list(bpmn_process_instance.get_tasks(TaskState.READY)) ready_tasks = list(bpmn_process_instance.get_tasks(state=TaskState.READY))
if len(ready_tasks) > 0: if len(ready_tasks) > 0:
return ready_tasks[0] return ready_tasks[0]
return None return None

View File

@ -9,7 +9,7 @@ from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer
from SpiffWorkflow.exceptions import WorkflowException # type: ignore from SpiffWorkflow.exceptions import WorkflowException # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.task import TaskStateNames from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel
from spiffworkflow_backend.models.bpmn_process import BpmnProcessNotFoundError from spiffworkflow_backend.models.bpmn_process import BpmnProcessNotFoundError
from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel
@ -136,6 +136,9 @@ class TaskService:
spiff_task: SpiffTask, spiff_task: SpiffTask,
) -> None: ) -> None:
for child_spiff_task in spiff_task.children: for child_spiff_task in spiff_task.children:
if child_spiff_task.has_state(TaskState.PREDICTED_MASK):
self.__class__.remove_spiff_task_from_parent(child_spiff_task, self.task_models)
continue
self.update_task_model_with_spiff_task( self.update_task_model_with_spiff_task(
spiff_task=child_spiff_task, spiff_task=child_spiff_task,
) )
@ -265,7 +268,7 @@ class TaskService:
spiff_task_data = new_properties_json.pop("data") spiff_task_data = new_properties_json.pop("data")
python_env_data_dict = self.__class__._get_python_env_data_dict_from_spiff_task(spiff_task, self.serializer) python_env_data_dict = self.__class__._get_python_env_data_dict_from_spiff_task(spiff_task, self.serializer)
task_model.properties_json = new_properties_json task_model.properties_json = new_properties_json
task_model.state = TaskStateNames[new_properties_json["state"]] task_model.state = TaskState.get_name(new_properties_json["state"])
json_data_dict = self.__class__.update_task_data_on_task_model_and_return_dict_if_updated( json_data_dict = self.__class__.update_task_data_on_task_model_and_return_dict_if_updated(
task_model, spiff_task_data, "json_data_hash" task_model, spiff_task_data, "json_data_hash"
) )
@ -430,6 +433,9 @@ class TaskService:
# we are going to avoid saving likely and maybe tasks to the db. # we are going to avoid saving likely and maybe tasks to the db.
# that means we need to remove them from their parents' lists of children as well. # that means we need to remove them from their parents' lists of children as well.
spiff_task = spiff_workflow.get_task_from_id(UUID(task_id)) spiff_task = spiff_workflow.get_task_from_id(UUID(task_id))
if spiff_task.has_state(TaskState.PREDICTED_MASK):
self.__class__.remove_spiff_task_from_parent(spiff_task, self.task_models)
continue
task_model = TaskModel.query.filter_by(guid=task_id).first() task_model = TaskModel.query.filter_by(guid=task_id).first()
if task_model is None: if task_model is None:

View File

@ -16,7 +16,7 @@ from SpiffWorkflow.bpmn.specs.event_definitions.message import MessageEventDefin
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.exceptions import SpiffWorkflowException # type: ignore from SpiffWorkflow.exceptions import SpiffWorkflowException # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.task import TaskState from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.message_instance import MessageInstanceModel from spiffworkflow_backend.models.message_instance import MessageInstanceModel
@ -166,7 +166,7 @@ class ExecutionStrategy:
self.delegate.save(bpmn_process_instance) self.delegate.save(bpmn_process_instance)
def get_ready_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]: def get_ready_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]:
tasks = [t for t in bpmn_process_instance.get_tasks(TaskState.READY) if not t.task_spec.manual] tasks = [t for t in bpmn_process_instance.get_tasks(state=TaskState.READY) if not t.task_spec.manual]
if len(tasks) > 0: if len(tasks) > 0:
self.subprocess_spec_loader() self.subprocess_spec_loader()
@ -249,7 +249,7 @@ class TaskModelSavingDelegate(EngineStepDelegate):
# but it didn't quite work in all cases, so we deleted it. you can find it in commit # but it didn't quite work in all cases, so we deleted it. you can find it in commit
# 1ead87b4b496525df8cc0e27836c3e987d593dc0 if you are curious. # 1ead87b4b496525df8cc0e27836c3e987d593dc0 if you are curious.
for waiting_spiff_task in bpmn_process_instance.get_tasks( for waiting_spiff_task in bpmn_process_instance.get_tasks(
TaskState.WAITING state=TaskState.WAITING
| TaskState.CANCELLED | TaskState.CANCELLED
| TaskState.READY | TaskState.READY
| TaskState.MAYBE | TaskState.MAYBE

View File

@ -2,7 +2,7 @@ from datetime import datetime
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.task import TaskState from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend.specs.start_event import StartConfiguration from spiffworkflow_backend.specs.start_event import StartConfiguration
from spiffworkflow_backend.specs.start_event import StartEvent from spiffworkflow_backend.specs.start_event import StartEvent
@ -10,7 +10,7 @@ from spiffworkflow_backend.specs.start_event import StartEvent
class WorkflowService: class WorkflowService:
@classmethod @classmethod
def future_start_events(cls, workflow: BpmnWorkflow) -> list[SpiffTask]: def future_start_events(cls, workflow: BpmnWorkflow) -> list[SpiffTask]:
return [t for t in workflow.get_tasks(TaskState.FUTURE) if isinstance(t.task_spec, StartEvent)] return [t for t in workflow.get_tasks(state=TaskState.FUTURE) if isinstance(t.task_spec, StartEvent)]
@classmethod @classmethod
def next_start_event_configuration(cls, workflow: BpmnWorkflow, now_in_utc: datetime) -> StartConfiguration | None: def next_start_event_configuration(cls, workflow: BpmnWorkflow, now_in_utc: datetime) -> StartConfiguration | None:

View File

@ -9,7 +9,7 @@ from typing import Any
import pytest import pytest
from flask.app import Flask from flask.app import Flask
from flask.testing import FlaskClient from flask.testing import FlaskClient
from SpiffWorkflow.task import TaskState # type: ignore from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend.exceptions.process_entity_not_found_error import ProcessEntityNotFoundError from spiffworkflow_backend.exceptions.process_entity_not_found_error import ProcessEntityNotFoundError
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.process_group import ProcessGroup from spiffworkflow_backend.models.process_group import ProcessGroup
@ -1617,7 +1617,7 @@ class TestProcessApi(BaseTest):
assert len(ready_tasks) == 1 assert len(ready_tasks) == 1
ready_task = ready_tasks[0] ready_task = ready_tasks[0]
# check all_tasks here to ensure we actually deleted items when cancelling the instance # check all_tasks here to ensure we actually deleted item when cancelling the instance
all_tasks = TaskModel.query.filter_by(process_instance_id=process_instance_id).all() all_tasks = TaskModel.query.filter_by(process_instance_id=process_instance_id).all()
assert len(all_tasks) == 8 assert len(all_tasks) == 8

View File

@ -28,7 +28,7 @@ class TestDotNotation(BaseTest):
processor.do_engine_steps(save=True) processor.do_engine_steps(save=True)
human_task = process_instance.human_tasks[0] human_task = process_instance.human_tasks[0]
user_task = processor.get_ready_user_tasks()[0] user_task = processor.get_all_ready_or_waiting_tasks()[0]
form_data = { form_data = {
"invoice.contibutorName": "Elizabeth", "invoice.contibutorName": "Elizabeth",
"invoice.contributorId": 100, "invoice.contributorId": 100,

View File

@ -52,7 +52,7 @@ class TestJinjaService(BaseTest):
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True) processor.do_engine_steps(save=True)
JinjaService.render_instructions_for_end_user(processor.get_ready_user_tasks()[0]) JinjaService.render_instructions_for_end_user(processor.get_all_ready_or_waiting_tasks()[0])
"\n".join( "\n".join(
[ [
r"* From Filter: Sanitized \| from \| filter", r"* From Filter: Sanitized \| from \| filter",

View File

@ -5,7 +5,7 @@ from flask import g
from flask.app import Flask from flask.app import Flask
from flask.testing import FlaskClient from flask.testing import FlaskClient
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.task import TaskState from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
@ -500,7 +500,7 @@ class TestProcessInstanceProcessor(BaseTest):
assert gateway_task is not None assert gateway_task is not None
assert gateway_task.state == TaskState.READY assert gateway_task.state == TaskState.READY
gateway_task = processor.bpmn_process_instance.get_tasks(TaskState.READY)[0] gateway_task = processor.bpmn_process_instance.get_tasks(state=TaskState.READY)[0]
processor.manual_complete_task(str(gateway_task.id), execute=True, user=process_instance.process_initiator) processor.manual_complete_task(str(gateway_task.id), execute=True, user=process_instance.process_initiator)
processor.save() processor.save()
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
@ -659,7 +659,9 @@ class TestProcessInstanceProcessor(BaseTest):
.filter(TaskDefinitionModel.bpmn_identifier == spiff_task.task_spec.name) .filter(TaskDefinitionModel.bpmn_identifier == spiff_task.task_spec.name)
.count() .count()
) )
assert task_models_with_bpmn_identifier_count < 3, count_failure_message
# some tasks will have 2 COMPLETED and 1 LIKELY/MAYBE
assert task_models_with_bpmn_identifier_count < 4, count_failure_message
task_model = TaskModel.query.filter_by(guid=str(spiff_task.id)).first() task_model = TaskModel.query.filter_by(guid=str(spiff_task.id)).first()
assert task_model.start_in_seconds is not None assert task_model.start_in_seconds is not None
@ -737,8 +739,7 @@ class TestProcessInstanceProcessor(BaseTest):
.filter(TaskModel.state.in_(["LIKELY", "MAYBE"])) # type: ignore .filter(TaskModel.state.in_(["LIKELY", "MAYBE"])) # type: ignore
.count() .count()
) )
assert task_models_that_are_predicted_count == 0 assert task_models_that_are_predicted_count == 4
assert processor_final.get_data() == data_set_7 assert processor_final.get_data() == data_set_7
def test_does_not_recreate_human_tasks_on_multiple_saves( def test_does_not_recreate_human_tasks_on_multiple_saves(

View File

@ -41,6 +41,7 @@ class TestTaskService(BaseTest):
bpmn_process_level_2b = ( bpmn_process_level_2b = (
BpmnProcessModel.query.join(BpmnProcessDefinitionModel) BpmnProcessModel.query.join(BpmnProcessDefinitionModel)
.filter(BpmnProcessDefinitionModel.bpmn_identifier == "Level2b") .filter(BpmnProcessDefinitionModel.bpmn_identifier == "Level2b")
.order_by(BpmnProcessModel.id)
.first() .first()
) )
assert bpmn_process_level_2b is not None assert bpmn_process_level_2b is not None
@ -50,6 +51,7 @@ class TestTaskService(BaseTest):
bpmn_process_level_3 = ( bpmn_process_level_3 = (
BpmnProcessModel.query.join(BpmnProcessDefinitionModel) BpmnProcessModel.query.join(BpmnProcessDefinitionModel)
.filter(BpmnProcessDefinitionModel.bpmn_identifier == "Level3") .filter(BpmnProcessDefinitionModel.bpmn_identifier == "Level3")
.order_by(BpmnProcessModel.id)
.first() .first()
) )
assert bpmn_process_level_3 is not None assert bpmn_process_level_3 is not None
@ -86,6 +88,7 @@ class TestTaskService(BaseTest):
task_model_level_2b = ( task_model_level_2b = (
TaskModel.query.join(TaskDefinitionModel) TaskModel.query.join(TaskDefinitionModel)
.filter(TaskDefinitionModel.bpmn_identifier == "level_2b_subprocess_script_task") .filter(TaskDefinitionModel.bpmn_identifier == "level_2b_subprocess_script_task")
.order_by(TaskModel.id)
.first() .first()
) )
assert task_model_level_2b is not None assert task_model_level_2b is not None
@ -100,6 +103,7 @@ class TestTaskService(BaseTest):
task_model_level_3 = ( task_model_level_3 = (
TaskModel.query.join(TaskDefinitionModel) TaskModel.query.join(TaskDefinitionModel)
.filter(TaskDefinitionModel.bpmn_identifier == "level_3_script_task") .filter(TaskDefinitionModel.bpmn_identifier == "level_3_script_task")
.order_by(TaskModel.id)
.first() .first()
) )
assert task_model_level_3 is not None assert task_model_level_3 is not None

View File

@ -16,7 +16,10 @@ test('renders hotCrumbs', () => {
render( render(
<BrowserRouter> <BrowserRouter>
<ProcessBreadcrumb <ProcessBreadcrumb
hotCrumbs={[['Process Groups', '/process-groups'], [`Process Group: hey`]]} hotCrumbs={[
['Process Groups', '/process-groups'],
[`Process Group: hey`],
]}
/> />
</BrowserRouter> </BrowserRouter>
); );