do not perform any tasks if instance is suspended from the interstitial page w/ burnettk

This commit is contained in:
jasquat 2023-05-05 14:01:32 -04:00
parent 6cdc6913c4
commit eef920acae
3 changed files with 50 additions and 37 deletions

View File

@ -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_instance_event import ProcessInstanceEventType
from spiffworkflow_backend.models.process_model import ProcessModelInfo 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.models.user import UserModel
from spiffworkflow_backend.routes.process_api_blueprint import ( from spiffworkflow_backend.routes.process_api_blueprint import (
_find_principal_or_raise, _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) 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]) -> str:
return_hash: dict = {"type": type} return_hash: dict = {"type": return_type}
return_hash[return_type] = entity return_hash[return_type] = entity
return f"data: {current_app.json.dumps(return_hash)} \n\n" 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)}", message=f"Failed to complete an automated task. Error was: {str(e)}",
status_code=400, status_code=400,
) )
yield render_data('error', api_error) yield render_data("error", api_error)
raise e raise e
if instructions and spiff_task.id not in reported_ids: if instructions and spiff_task.id not in reported_ids:
task = ProcessInstanceService.spiff_task_to_api_task(processor, spiff_task) task = ProcessInstanceService.spiff_task_to_api_task(processor, spiff_task)
task.properties = {"instructionsForEndUser": instructions} task.properties = {"instructionsForEndUser": instructions}
yield render_data('task', task) yield render_data("task", task)
reported_ids.append(spiff_task.id) reported_ids.append(spiff_task.id)
if spiff_task.state == TaskState.READY: 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: try:
processor.do_engine_steps(execution_strategy_name="one_at_a_time") processor.do_engine_steps(execution_strategy_name="one_at_a_time")
processor.do_engine_steps(execution_strategy_name="run_until_user_message") 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( api_error = ApiError.from_workflow_exception(
"engine_steps_error", "Failed complete an automated task.", exp=wfe "engine_steps_error", "Failed complete an automated task.", exp=wfe
) )
yield render_data('error', api_error) yield render_data("error", api_error)
return return
except Exception as e: except Exception as e:
api_error = ApiError( 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)}", message=f"Failed to complete an automated task. Error was: {str(e)}",
status_code=400, status_code=400,
) )
yield render_data('error', api_error) yield render_data("error", api_error)
return return
processor.refresh_waiting_tasks() processor.refresh_waiting_tasks()
ready_engine_task_count = get_ready_engine_step_count(processor.bpmn_process_instance) 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)}", message=f"Failed to complete an automated task. Error was: {str(e)}",
status_code=400, status_code=400,
) )
yield render_data('error', api_error) yield render_data("error", api_error)
raise e raise e
task.properties = {"instructionsForEndUser": instructions} 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: 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]: 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) 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): with ProcessInstanceQueueService.dequeued(process_instance):
yield from _interstitial_stream(process_instance) yield from _interstitial_stream(process_instance)

View File

@ -1688,14 +1688,15 @@ class TestProcessApi(BaseTest):
# The second script task should produce rendered jinja text # The second script task should produce rendered jinja text
# The Manual Task should then return a message as well. # The Manual Task should then return a message as well.
assert len(results) == 2 assert len(results) == 2
assert json_results[0]["state"] == "READY" # import pdb; pdb.set_trace()
assert json_results[0]["title"] == "Script Task #2" assert json_results[0]['task']["state"] == "READY"
assert json_results[0]["properties"]["instructionsForEndUser"] == "I am Script Task 2" assert json_results[0]['task']["title"] == "Script Task #2"
assert json_results[1]["state"] == "READY" assert json_results[0]['task']["properties"]["instructionsForEndUser"] == "I am Script Task 2"
assert json_results[1]["title"] == "Manual Task" assert json_results[1]['task']["state"] == "READY"
assert json_results[1]['task']["title"] == "Manual Task"
response = client.put( 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, headers=headers,
) )
@ -1705,14 +1706,14 @@ class TestProcessApi(BaseTest):
results = list(_dequeued_interstitial_stream(process_instance_id)) results = list(_dequeued_interstitial_stream(process_instance_id))
json_results = list(map(lambda x: json.loads(x[5:]), results)) # type: ignore json_results = list(map(lambda x: json.loads(x[5:]), results)) # type: ignore
assert len(results) == 1 assert len(results) == 1
assert json_results[0]["state"] == "READY" assert json_results[0]['task']["state"] == "READY"
assert json_results[0]["can_complete"] is False assert json_results[0]['task']["can_complete"] is False
assert json_results[0]["title"] == "Please Approve" assert json_results[0]['task']["title"] == "Please Approve"
assert json_results[0]["properties"]["instructionsForEndUser"] == "I am a manual task in another lane" assert json_results[0]['task']["properties"]["instructionsForEndUser"] == "I am a manual task in another lane"
# Complete task as the finance user. # Complete task as the finance user.
response = client.put( 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), headers=self.logged_in_headers(finance_user),
) )
@ -1722,8 +1723,8 @@ class TestProcessApi(BaseTest):
results = list(_dequeued_interstitial_stream(process_instance_id)) results = list(_dequeued_interstitial_stream(process_instance_id))
json_results = list(map(lambda x: json.loads(x[5:]), results)) # type: ignore json_results = list(map(lambda x: json.loads(x[5:]), results)) # type: ignore
assert len(json_results) == 1 assert len(json_results) == 1
assert json_results[0]["state"] == "COMPLETED" assert json_results[0]['task']["state"] == "COMPLETED"
assert json_results[0]["properties"]["instructionsForEndUser"] == "I am the end task" assert json_results[0]['task']["properties"]["instructionsForEndUser"] == "I am the end task"
def test_process_instance_list_with_default_list( def test_process_instance_list_with_default_list(
self, self,

View File

@ -9,12 +9,14 @@ import { getBasicHeaders } from '../services/HttpService';
// @ts-ignore // @ts-ignore
import InstructionsForEndUser from '../components/InstructionsForEndUser'; import InstructionsForEndUser from '../components/InstructionsForEndUser';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import { ProcessInstanceTask } from '../interfaces'; import { ProcessInstance, ProcessInstanceTask } from '../interfaces';
import useAPIError from '../hooks/UseApiError'; import useAPIError from '../hooks/UseApiError';
export default function ProcessInterstitial() { export default function ProcessInterstitial() {
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [lastTask, setLastTask] = useState<any>(null); const [lastTask, setLastTask] = useState<any>(null);
const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null);
const [state, setState] = useState<string>('RUNNING'); const [state, setState] = useState<string>('RUNNING');
const params = useParams(); const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
@ -31,17 +33,14 @@ export default function ProcessInterstitial() {
{ {
headers: getBasicHeaders(), headers: getBasicHeaders(),
onmessage(ev) { onmessage(ev) {
console.log('ev', ev);
const retValue = JSON.parse(ev.data); const retValue = JSON.parse(ev.data);
if (retValue.type === 'error') { if (retValue.type === 'error') {
addError(retValue.error); addError(retValue.error);
} else if (retValue.type === 'task') { } else if (retValue.type === 'task') {
setData((prevData) => [retValue.task, ...prevData]); setData((prevData) => [retValue.task, ...prevData]);
setLastTask(retValue); setLastTask(retValue.task);
// } else if (retValue.type === 'unrunnable_instance') { } else if (retValue.type === 'unrunnable_instance') {
// // setData((prevData) => [retValue.task, ...prevData]); setProcessInstance(retValue.unrunnable_instance);
// // setLastTask(retValue);
// setState('CLOSED');
} }
}, },
onclose() { onclose() {
@ -54,9 +53,14 @@ export default function ProcessInterstitial() {
const shouldRedirect = useCallback( const shouldRedirect = useCallback(
(myTask: ProcessInstanceTask): boolean => { (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(() => { useEffect(() => {
@ -73,6 +77,9 @@ export default function ProcessInterstitial() {
}, [lastTask, navigate, userTasks, shouldRedirect]); }, [lastTask, navigate, userTasks, shouldRedirect]);
const getStatus = (): string => { const getStatus = (): string => {
if (processInstance) {
return 'LOCKED';
}
if (!lastTask.can_complete && userTasks.includes(lastTask.type)) { if (!lastTask.can_complete && userTasks.includes(lastTask.type)) {
return 'LOCKED'; return 'LOCKED';
} }
@ -158,12 +165,15 @@ export default function ProcessInterstitial() {
return <div>{myTask.error_message}</div>; return <div>{myTask.error_message}</div>;
} }
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 ( return (
<div> <div>
<InstructionsForEndUser <InstructionsForEndUser task={myTask} defaultMessage={message} />
task={myTask}
defaultMessage="There are no additional instructions or information for this task."
/>
</div> </div>
); );
}; };