From 8e0630947d5463dac8faa8d4085fb27167c86d3e Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 4 May 2023 16:52:41 -0400 Subject: [PATCH 1/3] some attempts to not change the process instance status w/ burnettk --- .../models/process_instance.py | 2 +- .../routes/tasks_controller.py | 25 +++++++++++++------ spiffworkflow-frontend/src/interfaces.ts | 12 +++++++++ .../src/routes/ProcessInterstitial.tsx | 13 +++++++--- 4 files changed, 39 insertions(+), 13 deletions(-) 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..20a7e53f9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -45,7 +45,7 @@ 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, TaskModel # noqa: F401 from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.routes.process_api_blueprint import ( _find_principal_or_raise, @@ -400,6 +400,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]) -> str: + return_hash: dict = {"type": 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,12 +416,12 @@ 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: try: @@ -427,7 +432,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 +440,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 +459,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 +477,12 @@ 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) + if process_instance.status not in process_instance.__class__.active_statuses(): + yield f"data: {current_app.json.dumps(process_instance)} \n\n" + return + with ProcessInstanceQueueService.dequeued(process_instance): yield from _interstitial_stream(process_instance) 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..13cefa9fc 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInterstitial.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInterstitial.tsx @@ -31,12 +31,17 @@ export default function ProcessInterstitial() { { headers: getBasicHeaders(), onmessage(ev) { + console.log('ev', ev); const retValue = JSON.parse(ev.data); - if ('error_code' in retValue) { - addError(retValue); - } else { - setData((prevData) => [retValue, ...prevData]); + if (retValue.type === 'error') { + addError(retValue.error); + } else if (retValue.type === 'task') { + setData((prevData) => [retValue.task, ...prevData]); setLastTask(retValue); + // } else if (retValue.type === 'unrunnable_instance') { + // // setData((prevData) => [retValue.task, ...prevData]); + // // setLastTask(retValue); + // setState('CLOSED'); } }, onclose() { From eef920acae006ca1dc92447d976d54b09ea40d2d Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 5 May 2023 14:01:32 -0400 Subject: [PATCH 2/3] do not perform any tasks if instance is suspended from the interstitial page w/ burnettk --- .../routes/tasks_controller.py | 24 +++++++------ .../integration/test_process_api.py | 27 +++++++------- .../src/routes/ProcessInterstitial.tsx | 36 ++++++++++++------- 3 files changed, 50 insertions(+), 37 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index 20a7e53f9..73aeeb280 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 Task, 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, @@ -401,7 +402,7 @@ def _interstitial_stream(process_instance: ProcessInstanceModel) -> Generator[st return _render_instructions_for_end_user(task_model, extensions) def render_data(return_type: str, entity: Union[ApiError, Task]) -> str: - return_hash: dict = {"type": type} + return_hash: dict = {"type": return_type} return_hash[return_type] = entity return f"data: {current_app.json.dumps(return_hash)} \n\n" @@ -416,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 render_data('error', api_error) + 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 render_data('task', task) + 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") @@ -432,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 render_data('error', api_error) + yield render_data("error", api_error) return except Exception as e: api_error = ApiError( @@ -440,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 render_data('error', api_error) + yield render_data("error", api_error) return processor.refresh_waiting_tasks() ready_engine_task_count = get_ready_engine_step_count(processor.bpmn_process_instance) @@ -459,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 render_data('error', api_error) + yield render_data("error", api_error) raise e task.properties = {"instructionsForEndUser": instructions} - yield render_data('task', task) + yield render_data("task", task) def get_ready_engine_step_count(bpmn_process_instance: BpmnWorkflow) -> int: @@ -479,9 +484,6 @@ def get_ready_engine_step_count(bpmn_process_instance: BpmnWorkflow) -> int: 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) - if process_instance.status not in process_instance.__class__.active_statuses(): - yield f"data: {current_app.json.dumps(process_instance)} \n\n" - return 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..37c6272da 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/routes/ProcessInterstitial.tsx b/spiffworkflow-frontend/src/routes/ProcessInterstitial.tsx index 13cefa9fc..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(); @@ -31,17 +33,14 @@ export default function ProcessInterstitial() { { headers: getBasicHeaders(), onmessage(ev) { - console.log('ev', ev); const retValue = JSON.parse(ev.data); if (retValue.type === 'error') { addError(retValue.error); } else if (retValue.type === 'task') { setData((prevData) => [retValue.task, ...prevData]); - setLastTask(retValue); - // } else if (retValue.type === 'unrunnable_instance') { - // // setData((prevData) => [retValue.task, ...prevData]); - // // setLastTask(retValue); - // setState('CLOSED'); + setLastTask(retValue.task); + } else if (retValue.type === 'unrunnable_instance') { + setProcessInstance(retValue.unrunnable_instance); } }, onclose() { @@ -54,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(() => { @@ -73,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'; } @@ -158,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 (
- +
); }; From 874fe9052c2b56ba5b397016d4318780e583defa Mon Sep 17 00:00:00 2001 From: jasquat Date: Mon, 8 May 2023 13:20:40 -0400 Subject: [PATCH 3/3] pyl and i am not sure how mypy missed that one typing issue --- .../routes/tasks_controller.py | 2 +- .../integration/test_process_api.py | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index 73aeeb280..e3491cfb5 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -401,7 +401,7 @@ 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]) -> str: + 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" 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 37c6272da..ab3303c16 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -1689,11 +1689,11 @@ class TestProcessApi(BaseTest): # The Manual Task should then return a message as well. assert len(results) == 2 # 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" + 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]['task']['id']}", @@ -1706,10 +1706,10 @@ 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]['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" + 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( @@ -1723,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]['task']["state"] == "COMPLETED" - assert json_results[0]['task']["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,