diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py index b3ab709df..1668565c9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py @@ -172,7 +172,7 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel): @classmethod def active_statuses(cls) -> list[str]: - return ["user_input_required", "waiting"] + return ["not_started", "user_input_required", "waiting"] class ProcessInstanceModelSchema(Schema): diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index 66f93bb46..e3491cfb5 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -45,7 +45,8 @@ from spiffworkflow_backend.models.process_instance import ( ) from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType from spiffworkflow_backend.models.process_model import ProcessModelInfo -from spiffworkflow_backend.models.task import TaskModel # noqa: F401 +from spiffworkflow_backend.models.task import Task +from spiffworkflow_backend.models.task import TaskModel from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.routes.process_api_blueprint import ( _find_principal_or_raise, @@ -400,6 +401,11 @@ def _interstitial_stream(process_instance: ProcessInstanceModel) -> Generator[st extensions = TaskService.get_extensions_from_task_model(task_model) return _render_instructions_for_end_user(task_model, extensions) + def render_data(return_type: str, entity: Union[ApiError, Task, ProcessInstanceModel]) -> str: + return_hash: dict = {"type": return_type} + return_hash[return_type] = entity + return f"data: {current_app.json.dumps(return_hash)} \n\n" + tasks = get_reportable_tasks() while True: for spiff_task in tasks: @@ -411,14 +417,18 @@ def _interstitial_stream(process_instance: ProcessInstanceModel) -> Generator[st message=f"Failed to complete an automated task. Error was: {str(e)}", status_code=400, ) - yield f"data: {current_app.json.dumps(api_error)} \n\n" + yield render_data("error", api_error) raise e if instructions and spiff_task.id not in reported_ids: task = ProcessInstanceService.spiff_task_to_api_task(processor, spiff_task) task.properties = {"instructionsForEndUser": instructions} - yield f"data: {current_app.json.dumps(task)} \n\n" + yield render_data("task", task) reported_ids.append(spiff_task.id) if spiff_task.state == TaskState.READY: + # do not do any processing if the instance is not currently active + if process_instance.status not in ProcessInstanceModel.active_statuses(): + yield render_data("unrunnable_instance", process_instance) + break try: processor.do_engine_steps(execution_strategy_name="one_at_a_time") processor.do_engine_steps(execution_strategy_name="run_until_user_message") @@ -427,7 +437,7 @@ def _interstitial_stream(process_instance: ProcessInstanceModel) -> Generator[st api_error = ApiError.from_workflow_exception( "engine_steps_error", "Failed complete an automated task.", exp=wfe ) - yield f"data: {current_app.json.dumps(api_error)} \n\n" + yield render_data("error", api_error) return except Exception as e: api_error = ApiError( @@ -435,7 +445,7 @@ def _interstitial_stream(process_instance: ProcessInstanceModel) -> Generator[st message=f"Failed to complete an automated task. Error was: {str(e)}", status_code=400, ) - yield f"data: {current_app.json.dumps(api_error)} \n\n" + yield render_data("error", api_error) return processor.refresh_waiting_tasks() ready_engine_task_count = get_ready_engine_step_count(processor.bpmn_process_instance) @@ -454,10 +464,10 @@ def _interstitial_stream(process_instance: ProcessInstanceModel) -> Generator[st message=f"Failed to complete an automated task. Error was: {str(e)}", status_code=400, ) - yield f"data: {current_app.json.dumps(api_error)} \n\n" + yield render_data("error", api_error) raise e task.properties = {"instructionsForEndUser": instructions} - yield f"data: {current_app.json.dumps(task)} \n\n" + yield render_data("task", task) def get_ready_engine_step_count(bpmn_process_instance: BpmnWorkflow) -> int: @@ -472,8 +482,9 @@ def get_ready_engine_step_count(bpmn_process_instance: BpmnWorkflow) -> int: ) -def _dequeued_interstitial_stream(process_instance_id: int) -> Generator[str, Optional[str], None]: +def _dequeued_interstitial_stream(process_instance_id: int) -> Generator[Optional[str], Optional[str], None]: process_instance = _find_process_instance_by_id_or_raise(process_instance_id) + with ProcessInstanceQueueService.dequeued(process_instance): yield from _interstitial_stream(process_instance) 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 9a124f6d3..ab3303c16 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -1688,14 +1688,15 @@ class TestProcessApi(BaseTest): # The second script task should produce rendered jinja text # The Manual Task should then return a message as well. assert len(results) == 2 - assert json_results[0]["state"] == "READY" - assert json_results[0]["title"] == "Script Task #2" - assert json_results[0]["properties"]["instructionsForEndUser"] == "I am Script Task 2" - assert json_results[1]["state"] == "READY" - assert json_results[1]["title"] == "Manual Task" + # import pdb; pdb.set_trace() + assert json_results[0]["task"]["state"] == "READY" + assert json_results[0]["task"]["title"] == "Script Task #2" + assert json_results[0]["task"]["properties"]["instructionsForEndUser"] == "I am Script Task 2" + assert json_results[1]["task"]["state"] == "READY" + assert json_results[1]["task"]["title"] == "Manual Task" response = client.put( - f"/v1.0/tasks/{process_instance_id}/{json_results[1]['id']}", + f"/v1.0/tasks/{process_instance_id}/{json_results[1]['task']['id']}", headers=headers, ) @@ -1705,14 +1706,14 @@ class TestProcessApi(BaseTest): results = list(_dequeued_interstitial_stream(process_instance_id)) json_results = list(map(lambda x: json.loads(x[5:]), results)) # type: ignore assert len(results) == 1 - assert json_results[0]["state"] == "READY" - assert json_results[0]["can_complete"] is False - assert json_results[0]["title"] == "Please Approve" - assert json_results[0]["properties"]["instructionsForEndUser"] == "I am a manual task in another lane" + assert json_results[0]["task"]["state"] == "READY" + assert json_results[0]["task"]["can_complete"] is False + assert json_results[0]["task"]["title"] == "Please Approve" + assert json_results[0]["task"]["properties"]["instructionsForEndUser"] == "I am a manual task in another lane" # Complete task as the finance user. response = client.put( - f"/v1.0/tasks/{process_instance_id}/{json_results[0]['id']}", + f"/v1.0/tasks/{process_instance_id}/{json_results[0]['task']['id']}", headers=self.logged_in_headers(finance_user), ) @@ -1722,8 +1723,8 @@ class TestProcessApi(BaseTest): results = list(_dequeued_interstitial_stream(process_instance_id)) json_results = list(map(lambda x: json.loads(x[5:]), results)) # type: ignore assert len(json_results) == 1 - assert json_results[0]["state"] == "COMPLETED" - assert json_results[0]["properties"]["instructionsForEndUser"] == "I am the end task" + assert json_results[0]["task"]["state"] == "COMPLETED" + assert json_results[0]["task"]["properties"]["instructionsForEndUser"] == "I am the end task" def test_process_instance_list_with_default_list( self, diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index 79866cd6b..ac0980a28 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -351,3 +351,15 @@ export interface ProcessModelCaller { } export interface UserGroup {} + +type InterstitialPageResponseType = + | 'task_update' + | 'error' + | 'unrunnable_instance'; + +export interface InterstitialPageResponse { + type: InterstitialPageResponseType; + error?: any; + task?: ProcessInstanceTask; + process_instance?: ProcessInstance; +} diff --git a/spiffworkflow-frontend/src/routes/ProcessInterstitial.tsx b/spiffworkflow-frontend/src/routes/ProcessInterstitial.tsx index a5f6b3d33..1998c924f 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInterstitial.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInterstitial.tsx @@ -9,12 +9,14 @@ import { getBasicHeaders } from '../services/HttpService'; // @ts-ignore import InstructionsForEndUser from '../components/InstructionsForEndUser'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; -import { ProcessInstanceTask } from '../interfaces'; +import { ProcessInstance, ProcessInstanceTask } from '../interfaces'; import useAPIError from '../hooks/UseApiError'; export default function ProcessInterstitial() { const [data, setData] = useState([]); const [lastTask, setLastTask] = useState(null); + const [processInstance, setProcessInstance] = + useState(null); const [state, setState] = useState('RUNNING'); const params = useParams(); const navigate = useNavigate(); @@ -32,11 +34,13 @@ export default function ProcessInterstitial() { headers: getBasicHeaders(), onmessage(ev) { const retValue = JSON.parse(ev.data); - if ('error_code' in retValue) { - addError(retValue); - } else { - setData((prevData) => [retValue, ...prevData]); - setLastTask(retValue); + if (retValue.type === 'error') { + addError(retValue.error); + } else if (retValue.type === 'task') { + setData((prevData) => [retValue.task, ...prevData]); + setLastTask(retValue.task); + } else if (retValue.type === 'unrunnable_instance') { + setProcessInstance(retValue.unrunnable_instance); } }, onclose() { @@ -49,9 +53,14 @@ export default function ProcessInterstitial() { const shouldRedirect = useCallback( (myTask: ProcessInstanceTask): boolean => { - return myTask && myTask.can_complete && userTasks.includes(myTask.type); + return ( + !processInstance && + myTask && + myTask.can_complete && + userTasks.includes(myTask.type) + ); }, - [userTasks] + [userTasks, processInstance] ); useEffect(() => { @@ -68,6 +77,9 @@ export default function ProcessInterstitial() { }, [lastTask, navigate, userTasks, shouldRedirect]); const getStatus = (): string => { + if (processInstance) { + return 'LOCKED'; + } if (!lastTask.can_complete && userTasks.includes(lastTask.type)) { return 'LOCKED'; } @@ -153,12 +165,15 @@ export default function ProcessInterstitial() { return
{myTask.error_message}
; } + let message = + 'There are no additional instructions or information for this task.'; + if (processInstance && processInstance.status !== 'completed') { + message = `The tasks cannot be completed on this instance because its status is "${processInstance.status}".`; + } + return (
- +
); };