Multiinstance task data fix (#1497)

* Revert "Fix process data get subprocess (#1493)"

This reverts commit 84feef321d.

* added process model so we can test fixing loading task data with mi w/ burnettk

* added test and a potential fix w/ burnettk

* pyl is passing

* Update spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* put back api yml description

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
jasquat 2024-05-07 19:17:01 +00:00 committed by GitHub
parent ea502a9868
commit d86f3958f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 140 additions and 78 deletions

View File

@ -3,6 +3,7 @@ import os
import uuid import uuid
from typing import Any from typing import Any
from typing import TypedDict from typing import TypedDict
from uuid import UUID
import flask.wrappers import flask.wrappers
import sentry_sdk import sentry_sdk
@ -29,11 +30,9 @@ from spiffworkflow_backend.exceptions.error import HumanTaskAlreadyCompletedErro
from spiffworkflow_backend.exceptions.error import HumanTaskNotFoundError from spiffworkflow_backend.exceptions.error import HumanTaskNotFoundError
from spiffworkflow_backend.exceptions.error import UserDoesNotHaveAccessToTaskError from spiffworkflow_backend.exceptions.error import UserDoesNotHaveAccessToTaskError
from spiffworkflow_backend.exceptions.process_entity_not_found_error import ProcessEntityNotFoundError from spiffworkflow_backend.exceptions.process_entity_not_found_error import ProcessEntityNotFoundError
from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.human_task import HumanTaskModel from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel
from spiffworkflow_backend.models.json_data import JsonDataModel
from spiffworkflow_backend.models.principal import PrincipalModel from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
@ -136,35 +135,43 @@ def _process_data_fetcher(
bpmn_process_guid: str | None = None, bpmn_process_guid: str | None = None,
process_identifier: str | None = None, process_identifier: str | None = None,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
if process_identifier and bpmn_process_guid is None:
raise ApiError(
error_code="missing_required_parameter",
message="process_identifier was given but bpmn_process_guid was not. Both must be provided if either is required.",
status_code=404,
)
if process_identifier is None and bpmn_process_guid:
raise ApiError(
error_code="missing_required_parameter",
message="bpmn_process_guid was given but process_identifier was not. Both must be provided if either is required.",
status_code=404,
)
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 bpmn_process_guid is not None: processor = ProcessInstanceProcessor(process_instance)
bpmn_process = BpmnProcessModel.query.filter_by(guid=bpmn_process_guid).first()
else:
bpmn_process = process_instance.bpmn_process
if bpmn_process is None:
raise ApiError(
error_code="bpmn_process_not_found",
message=f"Cannot find a bpmn process with guid '{bpmn_process_guid}' for process instance {process_instance.id}",
status_code=404,
)
bpmn_process_data = JsonDataModel.find_data_dict_by_hash(bpmn_process.json_data_hash) bpmn_process_instance = processor.bpmn_process_instance
if bpmn_process_data is None: bpmn_process_data = processor.get_data()
raise ApiError( if process_identifier and bpmn_process_instance.spec.name != process_identifier:
error_code="bpmn_process_data_not_found", bpmn_process_instance = processor.bpmn_process_instance.subprocesses.get(UUID(bpmn_process_guid))
message=f"Cannot find a bpmn process data with guid '{bpmn_process_guid}' for process instance {process_instance.id}", if bpmn_process_instance is None:
status_code=404, raise ApiError(
) error_code="bpmn_process_not_found",
message=f"Cannot find a bpmn process with guid '{bpmn_process_guid}' for process instance {process_instance.id}",
status_code=404,
)
bpmn_process_data = bpmn_process_instance.data
data_objects = bpmn_process_data["data_objects"] data_objects = bpmn_process_instance.spec.data_objects
data_object = data_objects.get(process_data_identifier) data_object = data_objects.get(process_data_identifier)
if data_object is None: if data_object is None:
raise ApiError( raise ApiError(
error_code="data_object_not_found", error_code="data_object_not_found",
message=( message=(
f"Cannot find a data object with identifier '{process_data_identifier}' for bpmn process" f"Cannot find a data object with identifier '{process_data_identifier}' for bpmn process '{process_identifier}'"
f" '{bpmn_process.bpmn_process_definition.bpmn_identifier}' in process instance {process_instance.id}" f" in process instance {process_instance.id}"
), ),
status_code=404, status_code=404,
) )

View File

@ -693,19 +693,28 @@ class ProcessInstanceProcessor:
states_to_exclude_from_rehydration = ["COMPLETED", "ERROR"] states_to_exclude_from_rehydration = ["COMPLETED", "ERROR"]
task_list_by_hash = {t.guid: t for t in tasks} task_list_by_hash = {t.guid: t for t in tasks}
parent_task_guids = [] task_guids_to_add = set()
for task in tasks: for task in tasks:
parent_guid = task.parent_guid()
if task.state not in states_to_exclude_from_rehydration: if task.state not in states_to_exclude_from_rehydration:
json_data_hashes.add(task.json_data_hash) json_data_hashes.add(task.json_data_hash)
task_guids_to_add.add(task.guid)
# load parent task data to avoid certain issues that can arise from parallel branches # load parent task data to avoid certain issues that can arise from parallel branches
parent_guid = task.parent_guid()
if ( if (
parent_guid in task_list_by_hash parent_guid in task_list_by_hash
and task_list_by_hash[parent_guid].state in states_to_exclude_from_rehydration and task_list_by_hash[parent_guid].state in states_to_exclude_from_rehydration
): ):
json_data_hashes.add(task_list_by_hash[parent_guid].json_data_hash) json_data_hashes.add(task_list_by_hash[parent_guid].json_data_hash)
parent_task_guids.append(parent_guid) task_guids_to_add.add(parent_guid)
elif (
parent_guid in task_list_by_hash
and "instance_map" in (task_list_by_hash[parent_guid].runtime_info or {})
and task_list_by_hash[parent_guid] not in states_to_exclude_from_rehydration
):
# make sure we add task data for multi-instance tasks as well
json_data_hashes.add(task.json_data_hash)
task_guids_to_add.add(task.guid)
json_data_records = JsonDataModel.query.filter(JsonDataModel.hash.in_(json_data_hashes)).all() # type: ignore json_data_records = JsonDataModel.query.filter(JsonDataModel.hash.in_(json_data_hashes)).all() # type: ignore
json_data_mappings = {} json_data_mappings = {}
@ -718,7 +727,7 @@ class ProcessInstanceProcessor:
tasks_dict = spiff_bpmn_process_dict["subprocesses"][bpmn_subprocess_guid]["tasks"] tasks_dict = spiff_bpmn_process_dict["subprocesses"][bpmn_subprocess_guid]["tasks"]
tasks_dict[task.guid] = task.properties_json tasks_dict[task.guid] = task.properties_json
task_data = {} task_data = {}
if task.state not in states_to_exclude_from_rehydration or task.guid in parent_task_guids: if task.guid in task_guids_to_add:
task_data = json_data_mappings[task.json_data_hash] task_data = json_data_mappings[task.json_data_hash]
tasks_dict[task.guid]["data"] = task_data tasks_dict[task.guid]["data"] = task_data

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="Process_multiinstance_manual_task_eayacuw" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_17db3yp</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_17db3yp" sourceRef="StartEvent_1" targetRef="Activity_0wg6zvw" />
<bpmn:endEvent id="EndEvent_1">
<bpmn:incoming>Flow_12pkbxb</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_12pkbxb" sourceRef="manual_task" targetRef="EndEvent_1" />
<bpmn:manualTask id="manual_task" name="Manual Task">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>{{ the_input_var }}</spiffworkflow:instructionsForEndUser>
<spiffworkflow:preScript />
<spiffworkflow:postScript>the_output_var = the_input_var</spiffworkflow:postScript>
</bpmn:extensionElements>
<bpmn:incoming>Flow_01p4bbz</bpmn:incoming>
<bpmn:outgoing>Flow_12pkbxb</bpmn:outgoing>
<bpmn:multiInstanceLoopCharacteristics spiffworkflow:scriptsOnInstances="true">
<bpmn:loopDataInputRef>the_input</bpmn:loopDataInputRef>
<bpmn:loopDataOutputRef>z</bpmn:loopDataOutputRef>
<bpmn:inputDataItem id="the_input_var" name="the_input_var" />
<bpmn:outputDataItem id="the_output_var" name="the_output_var" />
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_01p4bbz" sourceRef="Activity_0wg6zvw" targetRef="manual_task" />
<bpmn:scriptTask id="Activity_0wg6zvw">
<bpmn:incoming>Flow_17db3yp</bpmn:incoming>
<bpmn:outgoing>Flow_01p4bbz</bpmn:outgoing>
<bpmn:script>the_input = ['a', 'b', 'c']</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_test_multistance_script_task_eayacuw">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="82" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_14za570_di" bpmnElement="EndEvent_1">
<dc:Bounds x="432" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0zqotmb_di" bpmnElement="manual_task">
<dc:Bounds x="270" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_05mv918_di" bpmnElement="Activity_0wg6zvw">
<dc:Bounds x="140" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_17db3yp_di" bpmnElement="Flow_17db3yp">
<di:waypoint x="118" y="177" />
<di:waypoint x="140" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_12pkbxb_di" bpmnElement="Flow_12pkbxb">
<di:waypoint x="370" y="177" />
<di:waypoint x="432" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_01p4bbz_di" bpmnElement="Flow_01p4bbz">
<di:waypoint x="240" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -5,7 +5,6 @@ import os
import time import time
from hashlib import sha256 from hashlib import sha256
from typing import Any from typing import Any
from unittest.mock import patch
import flask import flask
import pytest import pytest
@ -13,8 +12,6 @@ from flask.app import Flask
from flask.testing import FlaskClient from flask.testing import FlaskClient
from SpiffWorkflow.util.task import TaskState # type: ignore from SpiffWorkflow.util.task import TaskState # type: ignore
from spiffworkflow_backend.exceptions.process_entity_not_found_error import ProcessEntityNotFoundError from spiffworkflow_backend.exceptions.process_entity_not_found_error import ProcessEntityNotFoundError
from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel
from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
@ -3326,55 +3323,6 @@ class TestProcessApi(BaseTest):
assert response.json is not None assert response.json is not None
assert response.json["process_data_value"] == "hey" assert response.json["process_data_value"] == "hey"
def test_process_data_show_with_sub_process(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
process_model = load_test_spec(
"test_group/with-service-task-call-activity-sub-process",
process_model_source_directory="with-service-task-call-activity-sub-process",
)
process_instance_one = self.create_process_instance_from_process_model(process_model)
processor = ProcessInstanceProcessor(process_instance_one)
connector_response = {
"body": '{"ok": true}',
"mimetype": "application/json",
"http_status": 200,
"operator_identifier": "http/GetRequestV2",
}
with patch("requests.post") as mock_post:
mock_post.return_value.status_code = 200
mock_post.return_value.ok = True
mock_post.return_value.text = json.dumps(connector_response)
processor.do_engine_steps(save=True)
self.complete_next_manual_task(processor, execution_mode="synchronous")
self.complete_next_manual_task(processor, execution_mode="synchronous", data={"firstName": "Chuck"})
assert process_instance_one.status == "complete"
process_identifier = "call_activity_sub_process"
bpmn_processes = (
BpmnProcessModel.query.join(
BpmnProcessDefinitionModel, BpmnProcessDefinitionModel.id == BpmnProcessModel.bpmn_process_definition_id
)
.filter(BpmnProcessDefinitionModel.bpmn_identifier == process_identifier)
.all()
)
assert len(bpmn_processes) == 1
bpmn_process = bpmn_processes[0]
response = client.get(
f"/v1.0/process-data/default/{self.modify_process_identifier_for_path_param(process_model.id)}/sub_level_data_object_three/"
f"{process_instance_one.id}?process_identifier={process_identifier}&bpmn_process_guid={bpmn_process.guid}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
assert response.json is not None
assert response.json["process_data_value"] == "d"
def _setup_testing_instance( def _setup_testing_instance(
self, self,
client: FlaskClient, client: FlaskClient,

View File

@ -1018,3 +1018,38 @@ class TestProcessInstanceProcessor(BaseTest):
assert human_task_one.task_guid != str(non_manual_spiff_task.id) assert human_task_one.task_guid != str(non_manual_spiff_task.id)
with pytest.raises(TaskMismatchError): with pytest.raises(TaskMismatchError):
processor.complete_task(non_manual_spiff_task, human_task_one, user=process_instance.process_initiator) processor.complete_task(non_manual_spiff_task, human_task_one, user=process_instance.process_initiator)
def test_can_run_multiinstance_tasks_with_human_task(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
process_model = load_test_spec(
process_model_id="group/multiinstance_manual_task",
process_model_source_directory="multiinstance_manual_task",
)
process_instance = self.create_process_instance_from_process_model(process_model=process_model)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first()
processor = ProcessInstanceProcessor(process_instance)
human_task_one = process_instance.active_human_tasks[0]
spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id))
processor.complete_task(spiff_manual_task, human_task_one, user=process_instance.process_initiator)
process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first()
processor = ProcessInstanceProcessor(process_instance)
human_task_one = process_instance.active_human_tasks[0]
spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id))
processor.complete_task(spiff_manual_task, human_task_one, user=process_instance.process_initiator)
process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first()
processor = ProcessInstanceProcessor(process_instance)
human_task_one = process_instance.active_human_tasks[0]
spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id))
processor.complete_task(spiff_manual_task, human_task_one, user=process_instance.process_initiator)
processor.do_engine_steps(save=True)
assert process_instance.status == "complete"