From 60974ebfcb3adb876a9bff2aa2f311e6546988ba Mon Sep 17 00:00:00 2001
From: jasquat <jasquat@users.noreply.github.com>
Date: Thu, 11 May 2023 09:32:07 -0400
Subject: [PATCH] ensure we remove corresponding bpmn processes when removing
 tasks from a process reset

---
 spiffworkflow-backend/bin/query_tasks         |  4 ++--
 .../services/process_instance_processor.py    | 13 ++++++------
 .../services/task_service.py                  | 21 ++++++++++++++++---
 .../src/routes/ProcessInstanceShow.tsx        | 12 +++++++++--
 4 files changed, 37 insertions(+), 13 deletions(-)

diff --git a/spiffworkflow-backend/bin/query_tasks b/spiffworkflow-backend/bin/query_tasks
index 8cd6984e..43e701ae 100755
--- a/spiffworkflow-backend/bin/query_tasks
+++ b/spiffworkflow-backend/bin/query_tasks
@@ -10,11 +10,11 @@ set -o errtrace -o errexit -o nounset -o pipefail
 mysql -uroot spiffworkflow_backend_unit_testing -e '
   select * from process_instance;
 
-  select t.guid as task_guid, t.state as task_state, td.bpmn_identifier as task_id from task t
+  select t.guid as task_guid, t.state as task_state, td.bpmn_identifier as task_id, t.properties_json from task t
   join task_definition td on td.id = t.task_definition_id
   where process_instance_id=(select max(id) from process_instance);
 
-  select bp.guid as bp_guid, bpd.bpmn_identifier as bp_identifier from bpmn_process bp
+  select bp.guid as bp_guid, bpd.bpmn_identifier as bp_identifier, bp.properties_json from bpmn_process bp
   join bpmn_process_definition bpd on bpd.id = bp.bpmn_process_definition_id
   join bpmn_process bpb on bpb.id = bp.direct_parent_process_id
   join process_instance pi on bpb.id = pi.bpmn_process_id
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 ec64ad81..657965db 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py
@@ -1207,15 +1207,18 @@ class ProcessInstanceProcessor:
             process_instance, ProcessInstanceEventType.process_instance_rewound_to_task.value, task_guid=to_task_guid
         )
         processor = ProcessInstanceProcessor(process_instance)
-        deleted_tasks = processor.bpmn_process_instance.reset_from_task_id(UUID(to_task_guid))
-        spiff_tasks = processor.bpmn_process_instance.get_tasks()
+        initial_spiff_tasks = processor.bpmn_process_instance.get_tasks()
+
+        processor.bpmn_process_instance.reset_from_task_id(UUID(to_task_guid))
+        current_spiff_tasks = processor.bpmn_process_instance.get_tasks()
+        deleted_spiff_tasks = [t for t in initial_spiff_tasks if t not in current_spiff_tasks]
 
         task_service = TaskService(
             process_instance=processor.process_instance_model,
             serializer=processor._serializer,
             bpmn_definition_to_task_definitions_mappings=processor.bpmn_definition_to_task_definitions_mappings,
         )
-        task_service.update_all_tasks_from_spiff_tasks(spiff_tasks, deleted_tasks, start_time)
+        task_service.update_all_tasks_from_spiff_tasks(current_spiff_tasks, deleted_spiff_tasks, start_time)
 
         # Save the process
         processor.save()
@@ -1662,9 +1665,7 @@ class ProcessInstanceProcessor:
         """Complete_task."""
         task_model = TaskModel.query.filter_by(guid=human_task.task_id).first()
         if task_model is None:
-            raise TaskNotFoundError(
-                f"Cannot find a task with guid {self.process_instance_model.id} and task_id is {human_task.task_id}"
-            )
+            raise TaskNotFoundError(f"Cannot find a task with guid {human_task.task_id}")
 
         task_model.start_in_seconds = time.time()
         self.bpmn_process_instance.run_task_from_id(spiff_task.id)
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py
index d0b74d1d..135838e3 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py
@@ -29,6 +29,7 @@ from spiffworkflow_backend.models.process_instance_event import ProcessInstanceE
 from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
 from spiffworkflow_backend.models.spec_reference import SpecReferenceNotFoundError
 from spiffworkflow_backend.models.task import TaskModel  # noqa: F401
+from spiffworkflow_backend.models.task import TaskNotFoundError
 from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
 from spiffworkflow_backend.services.process_instance_tmp_service import ProcessInstanceTmpService
 
@@ -456,17 +457,27 @@ class TaskService:
     def update_all_tasks_from_spiff_tasks(
         self, spiff_tasks: list[SpiffTask], deleted_spiff_tasks: list[SpiffTask], start_time: float
     ) -> None:
+        """Update given spiff tasks in the database and remove deleted tasks."""
         # Remove all the deleted/pruned tasks from the database.
-        deleted_task_ids = list(map(lambda t: str(t.id), deleted_spiff_tasks))
-        tasks_to_clear = TaskModel.query.filter(TaskModel.guid.in_(deleted_task_ids)).all()  # type: ignore
+        deleted_task_guids = list(map(lambda t: str(t.id), deleted_spiff_tasks))
+        tasks_to_clear = TaskModel.query.filter(TaskModel.guid.in_(deleted_task_guids)).all()  # type: ignore
         human_tasks_to_clear = HumanTaskModel.query.filter(
-            HumanTaskModel.task_id.in_(deleted_task_ids)  # type: ignore
+            HumanTaskModel.task_id.in_(deleted_task_guids)  # type: ignore
         ).all()
 
         # delete human tasks first to avoid potential conflicts when deleting tasks.
         # otherwise sqlalchemy returns several warnings.
         for task in human_tasks_to_clear + tasks_to_clear:
             db.session.delete(task)
+        db.session.commit()
+
+        bpmn_processes_to_delete = (
+            BpmnProcessModel.query.filter(BpmnProcessModel.guid.in_(deleted_task_guids))  # type: ignore
+            .order_by(BpmnProcessModel.id.desc())  # type: ignore
+            .all()
+        )
+        for bpmn_process in bpmn_processes_to_delete:
+            db.session.delete(bpmn_process)
 
         # Note: Can't restrict this to definite, because some things are updated and are now CANCELLED
         # and other things may have been COMPLETED and are now MAYBE
@@ -570,6 +581,10 @@ class TaskService:
         bpmn_process_identifiers: list[str] = []
         if bpmn_process.guid:
             task_model = TaskModel.query.filter_by(guid=bpmn_process.guid).first()
+            if task_model is None:
+                raise TaskNotFoundError(
+                    f"Cannot find the corresponding task for the bpmn process with guid {bpmn_process.guid}."
+                )
             (
                 parent_bpmn_processes,
                 _task_models_of_parent_bpmn_processes,
diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx
index 48e87877..8650cc00 100644
--- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx
+++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 import Editor from '@monaco-editor/react';
 import {
   useParams,
@@ -40,6 +40,7 @@ import {
 import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
 import { useUriListForPermissions } from '../hooks/UriListForPermissions';
 import {
+  ErrorForDisplay,
   EventDefinition,
   PermissionsToCheck,
   ProcessData,
@@ -128,12 +129,18 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
     processInstanceLogListPageBaseUrl = `/admin/logs/${params.process_model_id}/${params.process_instance_id}`;
   }
 
+  const handleAddErrorInUseEffect = useCallback((value: ErrorForDisplay) => {
+    addError(value);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
   useEffect(() => {
     if (!permissionsLoaded) {
       return undefined;
     }
-    const processTaskFailure = () => {
+    const processTaskFailure = (result: any) => {
       setTasksCallHadError(true);
+      handleAddErrorInUseEffect(result);
     };
     const processTasksSuccess = (results: Task[]) => {
       if (params.to_task_guid) {
@@ -190,6 +197,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
     searchParams,
     taskListPath,
     variant,
+    handleAddErrorInUseEffect,
   ]);
 
   const deleteProcessInstance = () => {