diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index be9796aa..876b186e 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -798,7 +798,7 @@ paths: schema: $ref: "#/components/schemas/Workflow" - /process-instances/{modified_process_model_identifier}/{process_instance_id}/terminate: + /process-instance-terminate/{modified_process_model_identifier}/{process_instance_id}: parameters: - name: process_instance_id in: path @@ -819,7 +819,7 @@ paths: schema: $ref: "#/components/schemas/OkTrue" - /process-instances/{modified_process_model_identifier}/{process_instance_id}/suspend: + /process-instance-suspend/{modified_process_model_identifier}/{process_instance_id}: parameters: - name: process_instance_id in: path @@ -840,7 +840,7 @@ paths: schema: $ref: "#/components/schemas/OkTrue" - /process-instances/{modified_process_model_identifier}/{process_instance_id}/resume: + /process-instance-resume/{modified_process_model_identifier}/{process_instance_id}: parameters: - name: process_instance_id in: path diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py index 935eb209..1a4510b7 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py @@ -30,6 +30,10 @@ class ProcessInstanceTaskDataCannotBeUpdatedError(Exception): """ProcessInstanceTaskDataCannotBeUpdatedError.""" +class ProcessInstanceCannotBeDeletedError(Exception): + """ProcessInstanceCannotBeDeletedError.""" + + class NavigationItemSchema(Schema): """NavigationItemSchema.""" @@ -135,6 +139,15 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel): """Validate_status.""" return self.validate_enum_field(key, value, ProcessInstanceStatus) + def has_terminal_status(self) -> bool: + """Has_terminal_status.""" + return self.status in self.terminal_statuses() + + @classmethod + def terminal_statuses(cls) -> list[str]: + """Terminal_statuses.""" + return ["complete", "error", "terminated"] + class ProcessInstanceModelSchema(Schema): """ProcessInstanceModelSchema.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index f5a070b7..f2cb3e12 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -53,6 +53,9 @@ from spiffworkflow_backend.models.principal import PrincipalModel from spiffworkflow_backend.models.process_group import ProcessGroup from spiffworkflow_backend.models.process_group import ProcessGroupSchema from spiffworkflow_backend.models.process_instance import ProcessInstanceApiSchema +from spiffworkflow_backend.models.process_instance import ( + ProcessInstanceCannotBeDeletedError, +) from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus @@ -580,6 +583,13 @@ def process_instance_run( process_instance = ProcessInstanceService().get_process_instance( process_instance_id ) + if process_instance.status != "not_started": + raise ApiError( + error_code="process_instance_not_runnable", + message=f"Process Instance ({process_instance.id}) is currently running or has already run.", + status_code=400, + ) + processor = ProcessInstanceProcessor(process_instance) if do_engine_steps: @@ -938,7 +948,7 @@ def process_instance_list( if report_filter.initiated_by_me is True: process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(["complete", "error", "terminated"]) # type: ignore + ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore ) process_instance_query = process_instance_query.filter_by( process_initiator=g.user @@ -947,7 +957,7 @@ def process_instance_list( # TODO: not sure if this is exactly what is wanted if report_filter.with_tasks_completed_by_me is True: process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(["complete", "error", "terminated"]) # type: ignore + ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore ) # process_instance_query = process_instance_query.join(UserModel, UserModel.id == ProcessInstanceModel.process_initiator_id) # process_instance_query = process_instance_query.add_columns(UserModel.username) @@ -976,7 +986,7 @@ def process_instance_list( if report_filter.with_tasks_completed_by_my_group is True: process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(["complete", "error", "terminated"]) # type: ignore + ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore ) process_instance_query = process_instance_query.join( SpiffStepDetailsModel, @@ -1165,6 +1175,12 @@ def process_instance_delete( """Create_process_instance.""" process_instance = find_process_instance_by_id_or_raise(process_instance_id) + if not process_instance.has_terminal_status(): + raise ProcessInstanceCannotBeDeletedError( + f"Process instance ({process_instance.id}) cannot be deleted since it does not have a terminal status. " + f"Current status is {process_instance.status}." + ) + # (Pdb) db.session.delete # > db.session.query(SpiffLoggingModel).filter_by( diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index 3bc21456..1071ee25 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -1375,7 +1375,7 @@ class TestProcessApi(BaseTest): assert response.json is not None response = client.post( - f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/terminate", + f"/v1.0/process-instance-terminate/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 @@ -1396,15 +1396,13 @@ class TestProcessApi(BaseTest): ) -> None: """Test_process_instance_delete.""" process_group_id = "my_process_group" - process_model_id = "user_task" - bpmn_file_name = "user_task.bpmn" - bpmn_file_location = "user_task" + process_model_id = "sample" + bpmn_file_location = "sample" process_model_identifier = self.create_group_and_model_with_bpmn( client, with_super_admin_user, process_group_id=process_group_id, process_model_id=process_model_id, - bpmn_file_name=bpmn_file_name, bpmn_file_location=bpmn_file_location, ) @@ -1420,11 +1418,13 @@ class TestProcessApi(BaseTest): headers=self.logged_in_headers(with_super_admin_user), ) assert response.json is not None + assert response.status_code == 200 delete_response = client.delete( f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}", headers=self.logged_in_headers(with_super_admin_user), ) + assert delete_response.json["ok"] is True assert delete_response.status_code == 200 def test_task_show( @@ -2421,7 +2421,7 @@ class TestProcessApi(BaseTest): assert process_instance.status == "user_input_required" client.post( - f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/suspend", + f"/v1.0/process-instance-suspend/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}", headers=self.logged_in_headers(with_super_admin_user), ) process_instance = ProcessInstanceService().get_process_instance( @@ -2429,15 +2429,25 @@ class TestProcessApi(BaseTest): ) assert process_instance.status == "suspended" - # TODO: Why can I run a suspended process instance? response = client.post( f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", headers=self.logged_in_headers(with_super_admin_user), ) + process_instance = ProcessInstanceService().get_process_instance( + process_instance_id + ) + assert process_instance.status == "suspended" + assert response.status_code == 400 - # task = response.json['next_task'] - - print("test_process_instance_suspend") + response = client.post( + f"/v1.0/process-instance-resume/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}", + headers=self.logged_in_headers(with_super_admin_user), + ) + assert response.status_code == 200 + process_instance = ProcessInstanceService().get_process_instance( + process_instance_id + ) + assert process_instance.status == "waiting" def test_script_unit_test_run( self, diff --git a/spiffworkflow-frontend/src/classes/ProcessInstanceClass.tsx b/spiffworkflow-frontend/src/classes/ProcessInstanceClass.tsx new file mode 100644 index 00000000..d44569cd --- /dev/null +++ b/spiffworkflow-frontend/src/classes/ProcessInstanceClass.tsx @@ -0,0 +1,5 @@ +export default class ProcessInstanceClass { + static terminalStatuses() { + return ['complete', 'error', 'terminated']; + } +} diff --git a/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx b/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx index 4ba04352..a9895c71 100644 --- a/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx +++ b/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx @@ -11,6 +11,9 @@ export const useUriListForPermissions = () => { processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`, processInstanceCreatePath: `/v1.0/process-instances/${params.process_model_id}`, processInstanceActionPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}`, + processInstanceResumePath: `/v1.0/process-instance-resume/${params.process_model_id}/${params.process_instance_id}`, + processInstanceSuspendPath: `/v1.0/process-instance-suspend/${params.process_model_id}/${params.process_instance_id}`, + processInstanceTerminatePath: `/v1.0/process-instance-terminate/${params.process_model_id}/${params.process_instance_id}`, processInstanceListPath: '/v1.0/process-instances', processInstanceLogListPath: `/v1.0/logs/${params.process_model_id}/${params.process_instance_id}`, processInstanceReportListPath: '/v1.0/process-instances/reports', diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index 119f7964..db89bee8 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -45,6 +45,7 @@ import { ProcessInstanceTask, } from '../interfaces'; import { usePermissionFetcher } from '../hooks/PermissionService'; +import ProcessInstanceClass from '../classes/ProcessInstanceClass'; export default function ProcessInstanceShow() { const navigate = useNavigate(); @@ -74,9 +75,9 @@ export default function ProcessInstanceShow() { [targetUris.processInstanceActionPath]: ['DELETE'], [targetUris.processInstanceLogListPath]: ['GET'], [targetUris.processModelShowPath]: ['PUT'], - [`${targetUris.processInstanceActionPath}/suspend`]: ['POST'], - [`${targetUris.processInstanceActionPath}/terminate`]: ['POST'], - [`${targetUris.processInstanceActionPath}/resume`]: ['POST'], + [`${targetUris.processInstanceResumePath}`]: ['POST'], + [`${targetUris.processInstanceSuspendPath}`]: ['POST'], + [`${targetUris.processInstanceTerminatePath}`]: ['POST'], }; const { ability, permissionsLoaded } = usePermissionFetcher( permissionRequestData @@ -146,7 +147,7 @@ export default function ProcessInstanceShow() { const terminateProcessInstance = () => { HttpService.makeCallToBackend({ - path: `${targetUris.processInstanceActionPath}/terminate`, + path: `${targetUris.processInstanceTerminatePath}`, successCallback: refreshPage, httpMethod: 'POST', }); @@ -154,7 +155,7 @@ export default function ProcessInstanceShow() { const suspendProcessInstance = () => { HttpService.makeCallToBackend({ - path: `${targetUris.processInstanceActionPath}/suspend`, + path: `${targetUris.processInstanceSuspendPath}`, successCallback: refreshPage, httpMethod: 'POST', }); @@ -162,7 +163,7 @@ export default function ProcessInstanceShow() { const resumeProcessInstance = () => { HttpService.makeCallToBackend({ - path: `${targetUris.processInstanceActionPath}/resume`, + path: `${targetUris.processInstanceResumePath}`, successCallback: refreshPage, httpMethod: 'POST', }); @@ -333,7 +334,7 @@ export default function ProcessInstanceShow() { const terminateButton = () => { if ( processInstance && - !['complete', 'terminated', 'error'].includes(processInstance.status) + !ProcessInstanceClass.terminalStatuses().includes(processInstance.status) ) { return ( { if ( processInstance && - !['complete', 'terminated', 'error', 'suspended'].includes( - processInstance.status - ) + !ProcessInstanceClass.terminalStatuses() + .concat(['suspended']) + .includes(processInstance.status) ) { return (