diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py
index 7711c36f..eaf67f6c 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py
@@ -18,13 +18,13 @@ def setup_database_uri(app: Flask) -> None:
if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI") is None:
database_name = f"spiffworkflow_backend_{app.config['ENV_IDENTIFIER']}"
if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "sqlite":
- app.config["SQLALCHEMY_DATABASE_URI"] = (
- f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3"
- )
+ app.config[
+ "SQLALCHEMY_DATABASE_URI"
+ ] = f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3"
elif app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "postgres":
- app.config["SQLALCHEMY_DATABASE_URI"] = (
- f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}"
- )
+ app.config[
+ "SQLALCHEMY_DATABASE_URI"
+ ] = f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}"
else:
# use pswd to trick flake8 with hardcoded passwords
db_pswd = app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD")
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py
index b3ab709d..303532af 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py
@@ -129,9 +129,9 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
def serialized_with_metadata(self) -> dict[str, Any]:
process_instance_attributes = self.serialized
process_instance_attributes["process_metadata"] = self.process_metadata
- process_instance_attributes["process_model_with_diagram_identifier"] = (
- self.process_model_with_diagram_identifier
- )
+ process_instance_attributes[
+ "process_model_with_diagram_identifier"
+ ] = self.process_model_with_diagram_identifier
return process_instance_attributes
@property
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py
index 39d37c2c..d2f53232 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py
@@ -423,9 +423,9 @@ class ProcessInstanceProcessor:
tld.process_instance_id = process_instance_model.id
# we want this to be the fully qualified path to the process model including all group subcomponents
- current_app.config["THREAD_LOCAL_DATA"].process_model_identifier = (
- f"{process_instance_model.process_model_identifier}"
- )
+ current_app.config[
+ "THREAD_LOCAL_DATA"
+ ].process_model_identifier = f"{process_instance_model.process_model_identifier}"
self.process_instance_model = process_instance_model
self.process_model_service = ProcessModelService()
@@ -585,9 +585,9 @@ class ProcessInstanceProcessor:
bpmn_subprocess_definition.bpmn_identifier
] = bpmn_process_definition_dict
spiff_bpmn_process_dict["subprocess_specs"][bpmn_subprocess_definition.bpmn_identifier]["task_specs"] = {}
- bpmn_subprocess_definition_bpmn_identifiers[bpmn_subprocess_definition.id] = (
- bpmn_subprocess_definition.bpmn_identifier
- )
+ bpmn_subprocess_definition_bpmn_identifiers[
+ bpmn_subprocess_definition.id
+ ] = bpmn_subprocess_definition.bpmn_identifier
task_definitions = TaskDefinitionModel.query.filter(
TaskDefinitionModel.bpmn_process_definition_id.in_( # type: ignore
@@ -1115,59 +1115,54 @@ class ProcessInstanceProcessor:
def manual_complete_task(self, task_id: str, execute: bool) -> None:
"""Mark the task complete optionally executing it."""
- spiff_tasks_updated = {}
start_in_seconds = time.time()
spiff_task = self.bpmn_process_instance.get_task_from_id(UUID(task_id))
event_type = ProcessInstanceEventType.task_skipped.value
+ start_time = time.time()
+
if execute:
current_app.logger.info(
f"Manually executing Task {spiff_task.task_spec.name} of process"
f" instance {self.process_instance_model.id}"
)
- # Executing a subworkflow manually will restart its subprocess and allow stepping through it
+ # Executing a sub-workflow manually will restart its subprocess and allow stepping through it
if isinstance(spiff_task.task_spec, SubWorkflowTask):
subprocess = self.bpmn_process_instance.get_subprocess(spiff_task)
# We have to get to the actual start event
- for task in self.bpmn_process_instance.get_tasks(workflow=subprocess):
- task.complete()
- spiff_tasks_updated[task.id] = task
- if isinstance(task.task_spec, StartEvent):
+ for spiff_task in self.bpmn_process_instance.get_tasks(workflow=subprocess):
+ spiff_task.run()
+ if isinstance(spiff_task.task_spec, StartEvent):
break
else:
- spiff_task.complete()
- spiff_tasks_updated[spiff_task.id] = spiff_task
- for child in spiff_task.children:
- spiff_tasks_updated[child.id] = child
+ spiff_task.run()
event_type = ProcessInstanceEventType.task_executed_manually.value
else:
spiff_logger = logging.getLogger("spiff")
spiff_logger.info(f"Skipped task {spiff_task.task_spec.name}", extra=spiff_task.log_info())
- spiff_task._set_state(TaskState.COMPLETED)
- for child in spiff_task.children:
- child.task_spec._update(child)
- spiff_tasks_updated[child.id] = child
+ spiff_task.complete()
spiff_task.workflow.last_task = spiff_task
- spiff_tasks_updated[spiff_task.id] = spiff_task
-
end_in_seconds = time.time()
if isinstance(spiff_task.task_spec, EndEvent):
for task in self.bpmn_process_instance.get_tasks(TaskState.DEFINITE_MASK, workflow=spiff_task.workflow):
task.complete()
- spiff_tasks_updated[task.id] = task
# A subworkflow task will become ready when its workflow is complete. Engine steps would normally
# then complete it, but we have to do it ourselves here.
for task in self.bpmn_process_instance.get_tasks(TaskState.READY):
if isinstance(task.task_spec, SubWorkflowTask):
task.complete()
- spiff_tasks_updated[task.id] = task
task_service = TaskService(
process_instance=self.process_instance_model,
serializer=self._serializer,
bpmn_definition_to_task_definitions_mappings=self.bpmn_definition_to_task_definitions_mappings,
)
+
+ spiff_tasks_updated = {}
+ for task in self.bpmn_process_instance.get_tasks():
+ if task.last_state_change > start_time:
+ spiff_tasks_updated[task.id] = task
for updated_spiff_task in spiff_tasks_updated.values():
(
bpmn_process,
@@ -1216,6 +1211,14 @@ class ProcessInstanceProcessor:
raise TaskNotFoundError(
f"Cannot find a task with guid '{to_task_guid}' for process instance '{process_instance.id}'"
)
+ # If this task model has a parent boundary event, reset to that point instead,
+ # so we can reset all the boundary timers, etc...
+ parent_id = to_task_model.properties_json.get("parent", "")
+ parent = TaskModel.query.filter_by(guid=parent_id).first()
+ is_boundary_parent = False
+ if parent and parent.task_definition.typename == "_BoundaryEventParent":
+ to_task_model = parent
+ is_boundary_parent = True # Will need to complete this task at the end so we are on the correct process.
# NOTE: run ALL queries before making changes to ensure we get everything before anything changes
parent_bpmn_processes, task_models_of_parent_bpmn_processes = TaskService.task_models_of_parent_bpmn_processes(
@@ -1320,6 +1323,11 @@ class ProcessInstanceProcessor:
db.session.commit()
processor = ProcessInstanceProcessor(process_instance)
+
+ # If this as a boundary event parent, run it, so we get back to an active task.
+ if is_boundary_parent:
+ processor.do_engine_steps(execution_strategy_name="one_at_a_time")
+
processor.save()
processor.suspend()
diff --git a/spiffworkflow-backend/tests/data/boundary_event_reset/boundary_event_reset.bpmn b/spiffworkflow-backend/tests/data/boundary_event_reset/boundary_event_reset.bpmn
new file mode 100644
index 00000000..01ce0c5a
--- /dev/null
+++ b/spiffworkflow-backend/tests/data/boundary_event_reset/boundary_event_reset.bpmn
@@ -0,0 +1,51 @@
+
+
+
+
+ Flow_1ist4rn
+
+
+
+ Flow_1xbry1g
+
+
+
+ Flow_1ist4rn
+ Flow_0vzi07z
+
+
+
+ Flow_0vzi07z
+ Flow_1xbry1g
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spiffworkflow-backend/tests/data/boundary_event_reset/sub_with_timer.bpmn b/spiffworkflow-backend/tests/data/boundary_event_reset/sub_with_timer.bpmn
new file mode 100644
index 00000000..adffceda
--- /dev/null
+++ b/spiffworkflow-backend/tests/data/boundary_event_reset/sub_with_timer.bpmn
@@ -0,0 +1,89 @@
+
+
+
+
+ Flow_1e5apvr
+
+
+
+
+ Flow_110vf76
+
+
+
+ Flow_1hy0t7d
+
+ 'P14D'
+
+
+
+
+ Flow_1xbdri7
+
+
+
+ Flow_1e5apvr
+ Flow_0vtgres
+
+
+ Flow_1hy0t7d
+ Flow_1xbdri7
+
+
+ Flow_0vtgres
+ Flow_110vf76
+ y='1000'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py
index bfda1eb3..92e19919 100644
--- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py
@@ -434,6 +434,56 @@ class TestProcessInstanceProcessor(BaseTest):
assert process_instance.status == "complete"
+ def test_properly_resets_process_on_tasks_with_boundary_events(
+ self,
+ app: Flask,
+ client: FlaskClient,
+ with_db_and_bpmn_file_cleanup: None,
+ with_super_admin_user: UserModel,
+ ) -> None:
+ self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group")
+ process_model = load_test_spec(
+ process_model_id="test_group/boundary_event_reset",
+ process_model_source_directory="boundary_event_reset",
+ )
+ process_instance = self.create_process_instance_from_process_model(
+ process_model=process_model, user=with_super_admin_user
+ )
+ processor = ProcessInstanceProcessor(process_instance)
+ processor.do_engine_steps(save=True)
+ assert len(process_instance.active_human_tasks) == 1
+ human_task_one = process_instance.active_human_tasks[0]
+ spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id))
+ ProcessInstanceService.complete_form_task(
+ processor, spiff_manual_task, {}, with_super_admin_user, human_task_one
+ )
+ assert (
+ len(process_instance.active_human_tasks) == 1
+ ), "expected 1 active human tasks after 2nd one is completed"
+ assert process_instance.active_human_tasks[0].task_title == "Final"
+
+ # Reset the process back to the task within the call activity that contains a timer_boundary event.
+ reset_to_spiff_task: SpiffTask = processor.__class__.get_task_by_bpmn_identifier(
+ "manual_task_1", processor.bpmn_process_instance
+ )
+ processor.suspend()
+ processor = ProcessInstanceProcessor(process_instance)
+ ProcessInstanceProcessor.reset_process(process_instance, str(reset_to_spiff_task.id))
+ human_task_one = process_instance.active_human_tasks[0]
+ assert human_task_one.task_title == "Manual Task #1"
+ processor = ProcessInstanceProcessor(process_instance)
+ processor.manual_complete_task(str(spiff_manual_task.id), execute=True)
+ processor = ProcessInstanceProcessor(process_instance)
+ processor.resume()
+ processor.do_engine_steps(save=True)
+ process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first()
+
+ assert len(process_instance.active_human_tasks) == 1
+ assert process_instance.active_human_tasks[0].task_title == "Final", (
+ "once we reset, resume, and complete the task, we should be back to the Final step again, and not"
+ "stuck waiting for the call activity to complete (which was happening in a bug I'm fixing right now)"
+ )
+
def test_properly_saves_tasks_when_running(
self,
app: Flask,