diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 2a247863..304fa4e1 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1565,7 +1565,7 @@ paths: schema: type: string post: - operationId: spiffworkflow_backend.routes.process_api_blueprint.mark_task_complete + operationId: spiffworkflow_backend.routes.process_api_blueprint.manual_complete_task summary: Mark a task complete without executing it tags: - Process Instances 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 b8db2dab..2b1f1306 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -28,11 +28,6 @@ from lxml import etree # type: ignore from lxml.builder import ElementMaker # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import TaskState -from sqlalchemy import and_ -from sqlalchemy import asc -from sqlalchemy import desc -from sqlalchemy import or_ - from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( ProcessEntityNotFoundError, ) @@ -99,6 +94,10 @@ from spiffworkflow_backend.services.secret_service import SecretService from spiffworkflow_backend.services.service_task_service import ServiceTaskService from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.user_service import UserService +from sqlalchemy import and_ +from sqlalchemy import asc +from sqlalchemy import desc +from sqlalchemy import or_ class TaskDataSelectOption(TypedDict): @@ -2189,23 +2188,24 @@ def send_bpmn_event( ) -def mark_task_complete( +def manual_complete_task( modified_process_model_identifier: str, process_instance_id: str, task_id: str, body: Dict, ) -> Response: """Mark a task complete without executing it.""" + execute = body.get("execute", True) process_instance = ProcessInstanceModel.query.filter( ProcessInstanceModel.id == int(process_instance_id) ).first() if process_instance: processor = ProcessInstanceProcessor(process_instance) - processor.mark_task_complete(task_id) + processor.manual_complete_task(task_id, execute) else: raise ApiError( - error_code="send_bpmn_event_error", - message=f"Could not skip Task {task_id} in Instance {process_instance_id}", + error_code="complete_task", + message=f"Could not complete Task {task_id} in Instance {process_instance_id}", ) return Response( json.dumps(ProcessInstanceModelSchema().dump(process_instance)), 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 79de71f0..cd838d79 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -621,7 +621,7 @@ class ProcessInstanceProcessor: db.session.add(pim) db.session.commit() - def save(self) -> None: + def _save(self) -> None: """Saves the current state of this processor to the database.""" self.process_instance_model.bpmn_json = self.serialize() @@ -643,6 +643,9 @@ class ProcessInstanceProcessor: db.session.add(self.process_instance_model) db.session.commit() + def save(self) -> None: + """Saves the current state and moves on to the next state.""" + self._save() human_tasks = HumanTaskModel.query.filter_by( process_instance_id=self.process_instance_model.id ).all() @@ -729,17 +732,25 @@ class ProcessInstanceProcessor: self.bpmn_process_instance.catch(event_definition) self.do_engine_steps(save=True) - def mark_task_complete(self, task_id: str) -> None: - """Mark the task complete without executing it.""" + def manual_complete_task(self, task_id: str, execute: bool) -> None: + """Mark the task complete optionally executing it.""" spiff_task = self.bpmn_process_instance.get_task(UUID(task_id)) - spiff_task._set_state(TaskState.COMPLETED) + if execute: + current_app.logger.info( + f"Manually executing Task {spiff_task.task_spec.name} of process instance {self.process_instance_model.id}" + ) + spiff_task.complete() + else: + current_app.logger.info( + f"Skipping Task {spiff_task.task_spec.name} of process instance {self.process_instance_model.id}" + ) + spiff_task._set_state(TaskState.COMPLETED) + for child in spiff_task.children: + child.task_spec._update(child) self.bpmn_process_instance.last_task = spiff_task - for child in spiff_task.children: - child.task_spec._update(child) - current_app.logger.info( - f"Task {spiff_task.task_spec.name} of process instance {self.process_instance_model.id} skipped" - ) - self.do_engine_steps(save=True) + self._save() + # Saving the workflow seems to reset the status + self.suspend() @staticmethod def get_parser() -> MyCustomParser: diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index d984b664..678ebdf2 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -529,6 +529,8 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { // We actually could allow this for any waiting events const taskTypes = ['Event Based Gateway']; return ( + processInstance && + processInstance.status === 'waiting' && ability.can('POST', targetUris.processInstanceSendEventPath) && taskTypes.filter((t) => t === task.type).length > 0 && task.state === 'WAITING' && @@ -536,8 +538,10 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { ); }; - const canMarkTaskComplete = (task: any) => { + const canCompleteTask = (task: any) => { return ( + processInstance && + processInstance.status === 'suspended' && ability.can('POST', targetUris.processInstanceCompleteTaskPath) && task.state === 'READY' && showingLastSpiffStep() @@ -546,6 +550,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { const canResetProcess = (task: any) => { return ( + ability.can('POST', targetUris.processInstanceResetPath) && processInstance && processInstance.status === 'suspended' && task.state === 'READY' && @@ -627,13 +632,14 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { }); }; - const markTaskComplete = () => { + const completeTask = (execute: boolean) => { const taskToUse: any = taskToDisplay; HttpService.makeCallToBackend({ path: `/task-complete/${modifiedProcessModelId}/${params.process_instance_id}/${taskToUse.id}`, httpMethod: 'POST', successCallback: saveTaskDataResult, failureCallback: saveTaskDataFailure, + postBody: { execute }, }); }; @@ -705,15 +711,23 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { ); } - if (canMarkTaskComplete(task)) { + if (canCompleteTask(task)) { buttons.push( ); + buttons.push( + + ); } if (canSendEvent(task)) { buttons.push(