Merge pull request #78 from sartography/feature/process-navigation

Feature/process navigation
This commit is contained in:
Elizabeth Esswein 2022-12-29 20:00:27 -05:00 committed by GitHub
commit 9d7723df8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 783 additions and 28 deletions

View File

@ -1060,6 +1060,39 @@ paths:
schema: schema:
$ref: "#/components/schemas/OkTrue" $ref: "#/components/schemas/OkTrue"
/process-instance-reset/{modified_process_model_identifier}/{process_instance_id}/{spiff_step}:
parameters:
- name: modified_process_model_identifier
in: path
required: true
description: The modified process model id
schema:
type: string
- name: process_instance_id
in: path
required: true
description: The unique id of an existing process instance.
schema:
type: integer
- name: spiff_step
in: query
required: false
description: Reset the process to this state
schema:
type: integer
post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_reset
summary: Reset a process instance to an earlier step
tags:
- Process Instances
responses:
"200":
description: Empty ok true response on successful resume.
content:
application/json:
schema:
$ref: "#/components/schemas/OkTrue"
/process-instances/reports: /process-instances/reports:
parameters: parameters:
- name: page - name: page
@ -1484,6 +1517,66 @@ paths:
schema: schema:
$ref: "#/components/schemas/Workflow" $ref: "#/components/schemas/Workflow"
/send-event/{modified_process_model_identifier}/{process_instance_id}:
parameters:
- name: modified_process_model_identifier
in: path
required: true
description: The modified id of an existing process model
schema:
type: string
- name: process_instance_id
in: path
required: true
description: The unique id of the process instance
schema:
type: string
post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.send_bpmn_event
summary: Send a BPMN event to the process
tags:
- Process Instances
responses:
"200":
description: Event Sent Successfully
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/task-complete/{modified_process_model_identifier}/{process_instance_id}/{task_id}:
parameters:
- name: modified_process_model_identifier
in: path
required: true
description: The modified id of an existing process model
schema:
type: string
- name: process_instance_id
in: path
required: true
description: The unique id of the process instance
schema:
type: string
- name: task_id
in: path
required: true
description: The unique id of the task.
schema:
type: string
post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.manual_complete_task
summary: Mark a task complete without executing it
tags:
- Process Instances
responses:
"200":
description: Event Sent Successfully
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/service-tasks: /service-tasks:
get: get:
tags: tags:

View File

@ -118,6 +118,7 @@ class Task:
form_schema: Union[str, None] = None, form_schema: Union[str, None] = None,
form_ui_schema: Union[str, None] = None, form_ui_schema: Union[str, None] = None,
parent: Optional[str] = None, parent: Optional[str] = None,
event_definition: Union[dict[str, Any], None] = None,
call_activity_process_identifier: Optional[str] = None, call_activity_process_identifier: Optional[str] = None,
): ):
"""__init__.""" """__init__."""
@ -130,6 +131,7 @@ class Task:
self.documentation = documentation self.documentation = documentation
self.lane = lane self.lane = lane
self.parent = parent self.parent = parent
self.event_definition = event_definition
self.call_activity_process_identifier = call_activity_process_identifier self.call_activity_process_identifier = call_activity_process_identifier
self.data = data self.data = data
@ -189,6 +191,7 @@ class Task:
"form_schema": self.form_schema, "form_schema": self.form_schema,
"form_ui_schema": self.form_ui_schema, "form_ui_schema": self.form_ui_schema,
"parent": self.parent, "parent": self.parent,
"event_definition": self.event_definition,
"call_activity_process_identifier": self.call_activity_process_identifier, "call_activity_process_identifier": self.call_activity_process_identifier,
} }
@ -290,6 +293,7 @@ class TaskSchema(Schema):
"process_instance_id", "process_instance_id",
"form_schema", "form_schema",
"form_ui_schema", "form_ui_schema",
"event_definition",
] ]
multi_instance_type = EnumField(MultiInstanceType) multi_instance_type = EnumField(MultiInstanceType)

View File

@ -652,6 +652,46 @@ def process_instance_resume(
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def process_instance_reset(
process_instance_id: int,
modified_process_model_identifier: str,
spiff_step: int = 0,
) -> flask.wrappers.Response:
"""Process_instance_reset."""
process_instance = ProcessInstanceService().get_process_instance(
process_instance_id
)
step_detail = (
db.session.query(SpiffStepDetailsModel)
.filter(
SpiffStepDetailsModel.process_instance_id == process_instance.id,
SpiffStepDetailsModel.spiff_step == spiff_step,
)
.first()
)
if step_detail is not None and process_instance.bpmn_json is not None:
bpmn_json = json.loads(process_instance.bpmn_json)
bpmn_json["tasks"] = step_detail.task_json["tasks"]
bpmn_json["subprocesses"] = step_detail.task_json["subprocesses"]
process_instance.bpmn_json = json.dumps(bpmn_json)
db.session.add(process_instance)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
raise ApiError(
error_code="reset_process_instance_error",
message=f"Could not update the Instance. Original error is {e}",
) from e
return Response(
json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
status=200,
mimetype="application/json",
)
def process_instance_log_list( def process_instance_log_list(
modified_process_model_identifier: str, modified_process_model_identifier: str,
process_instance_id: int, process_instance_id: int,
@ -1406,7 +1446,7 @@ def process_instance_task_list(
step_detail = ( step_detail = (
db.session.query(SpiffStepDetailsModel) db.session.query(SpiffStepDetailsModel)
.filter( .filter(
SpiffStepDetailsModel.process_instance.id == process_instance.id, SpiffStepDetailsModel.process_instance_id == process_instance.id,
SpiffStepDetailsModel.spiff_step == spiff_step, SpiffStepDetailsModel.spiff_step == spiff_step,
) )
.first() .first()
@ -1427,7 +1467,7 @@ def process_instance_task_list(
tasks = [] tasks = []
for spiff_task in spiff_tasks: for spiff_task in spiff_tasks:
task = ProcessInstanceService.spiff_task_to_api_task(spiff_task) task = ProcessInstanceService.spiff_task_to_api_task(processor, spiff_task)
if get_task_data: if get_task_data:
task.data = spiff_task.data task.data = spiff_task.data
tasks.append(task) tasks.append(task)
@ -1461,7 +1501,9 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
form_schema_file_name = properties["formJsonSchemaFilename"] form_schema_file_name = properties["formJsonSchemaFilename"]
if "formUiSchemaFilename" in properties: if "formUiSchemaFilename" in properties:
form_ui_schema_file_name = properties["formUiSchemaFilename"] form_ui_schema_file_name = properties["formUiSchemaFilename"]
task = ProcessInstanceService.spiff_task_to_api_task(spiff_task)
processor = ProcessInstanceProcessor(process_instance)
task = ProcessInstanceService.spiff_task_to_api_task(processor, spiff_task)
task.data = spiff_task.data task.data = spiff_task.data
task.process_model_display_name = process_model.display_name task.process_model_display_name = process_model.display_name
task.process_model_identifier = process_model.id task.process_model_identifier = process_model.id
@ -2077,6 +2119,107 @@ def _update_form_schema_with_task_data_as_needed(
_update_form_schema_with_task_data_as_needed(o, task_data) _update_form_schema_with_task_data_as_needed(o, task_data)
def update_task_data(
process_instance_id: str,
modified_process_model_identifier: str,
task_id: str,
body: Dict,
) -> Response:
"""Update task data."""
process_instance = ProcessInstanceModel.query.filter(
ProcessInstanceModel.id == int(process_instance_id)
).first()
if process_instance:
if process_instance.status != "suspended":
raise ProcessInstanceTaskDataCannotBeUpdatedError(
f"The process instance needs to be suspended to udpate the task-data. It is currently: {process_instance.status}"
)
process_instance_bpmn_json_dict = json.loads(process_instance.bpmn_json)
if "new_task_data" in body:
new_task_data_str: str = body["new_task_data"]
new_task_data_dict = json.loads(new_task_data_str)
if task_id in process_instance_bpmn_json_dict["tasks"]:
process_instance_bpmn_json_dict["tasks"][task_id][
"data"
] = new_task_data_dict
process_instance.bpmn_json = json.dumps(process_instance_bpmn_json_dict)
db.session.add(process_instance)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
raise ApiError(
error_code="update_task_data_error",
message=f"Could not update the Instance. Original error is {e}",
) from e
else:
raise ApiError(
error_code="update_task_data_error",
message=f"Could not find Task: {task_id} in Instance: {process_instance_id}.",
)
else:
raise ApiError(
error_code="update_task_data_error",
message=f"Could not update task data for Instance: {process_instance_id}, and Task: {task_id}.",
)
return Response(
json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
status=200,
mimetype="application/json",
)
def send_bpmn_event(
modified_process_model_identifier: str,
process_instance_id: str,
body: Dict,
) -> Response:
"""Send a bpmn event to a workflow."""
process_instance = ProcessInstanceModel.query.filter(
ProcessInstanceModel.id == int(process_instance_id)
).first()
if process_instance:
processor = ProcessInstanceProcessor(process_instance)
processor.send_bpmn_event(body)
else:
raise ApiError(
error_code="send_bpmn_event_error",
message=f"Could not send event to Instance: {process_instance_id}",
)
return Response(
json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
status=200,
mimetype="application/json",
)
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.manual_complete_task(task_id, execute)
else:
raise ApiError(
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)),
status=200,
mimetype="application/json",
)
def _commit_and_push_to_git(message: str) -> None: def _commit_and_push_to_git(message: str) -> None:
"""Commit_and_push_to_git.""" """Commit_and_push_to_git."""
if current_app.config["GIT_COMMIT_ON_SAVE"]: if current_app.config["GIT_COMMIT_ON_SAVE"]:

View File

@ -17,6 +17,7 @@ from typing import Optional
from typing import Tuple from typing import Tuple
from typing import TypedDict from typing import TypedDict
from typing import Union from typing import Union
from uuid import UUID
import dateparser import dateparser
import pytz import pytz
@ -43,6 +44,9 @@ from SpiffWorkflow.spiff.serializer.task_spec_converters import (
CallActivityTaskConverter, CallActivityTaskConverter,
) )
from SpiffWorkflow.spiff.serializer.task_spec_converters import EndEventConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import EndEventConverter
from SpiffWorkflow.spiff.serializer.task_spec_converters import (
EventBasedGatewayConverter,
)
from SpiffWorkflow.spiff.serializer.task_spec_converters import ( from SpiffWorkflow.spiff.serializer.task_spec_converters import (
IntermediateCatchEventConverter, IntermediateCatchEventConverter,
) )
@ -265,6 +269,7 @@ class ProcessInstanceProcessor:
EndEventConverter, EndEventConverter,
IntermediateCatchEventConverter, IntermediateCatchEventConverter,
IntermediateThrowEventConverter, IntermediateThrowEventConverter,
EventBasedGatewayConverter,
ManualTaskConverter, ManualTaskConverter,
NoneTaskConverter, NoneTaskConverter,
ReceiveTaskConverter, ReceiveTaskConverter,
@ -278,6 +283,7 @@ class ProcessInstanceProcessor:
] ]
) )
_serializer = BpmnWorkflowSerializer(wf_spec_converter, version=SERIALIZER_VERSION) _serializer = BpmnWorkflowSerializer(wf_spec_converter, version=SERIALIZER_VERSION)
_event_serializer = EventBasedGatewayConverter()
PROCESS_INSTANCE_ID_KEY = "process_instance_id" PROCESS_INSTANCE_ID_KEY = "process_instance_id"
VALIDATION_PROCESS_KEY = "validate_only" VALIDATION_PROCESS_KEY = "validate_only"
@ -616,7 +622,7 @@ class ProcessInstanceProcessor:
db.session.add(pim) db.session.add(pim)
db.session.commit() db.session.commit()
def save(self) -> None: def _save(self) -> None:
"""Saves the current state of this processor to the database.""" """Saves the current state of this processor to the database."""
self.process_instance_model.bpmn_json = self.serialize() self.process_instance_model.bpmn_json = self.serialize()
@ -638,6 +644,9 @@ class ProcessInstanceProcessor:
db.session.add(self.process_instance_model) db.session.add(self.process_instance_model)
db.session.commit() 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( human_tasks = HumanTaskModel.query.filter_by(
process_instance_id=self.process_instance_model.id process_instance_id=self.process_instance_model.id
).all() ).all()
@ -706,6 +715,44 @@ class ProcessInstanceProcessor:
db.session.add(at) db.session.add(at)
db.session.commit() db.session.commit()
def serialize_task_spec(self, task_spec: SpiffTask) -> Any:
"""Get a serialized version of a task spec."""
# The task spec is NOT actually a SpiffTask, it is the task spec attached to a SpiffTask
# Not sure why mypy accepts this but whatever.
return self._serializer.spec_converter.convert(task_spec)
def send_bpmn_event(self, event_data: dict[str, Any]) -> None:
"""Send an event to the workflow."""
payload = event_data.pop("payload", None)
event_definition = self._event_serializer.restore(event_data)
if payload is not None:
event_definition.payload = payload
current_app.logger.info(
f"Event of type {event_definition.event_type} sent to process instance {self.process_instance_model.id}"
)
self.bpmn_process_instance.catch(event_definition)
self.do_engine_steps(save=True)
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))
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
self._save()
# Saving the workflow seems to reset the status
self.suspend()
@staticmethod @staticmethod
def get_parser() -> MyCustomParser: def get_parser() -> MyCustomParser:
"""Get_parser.""" """Get_parser."""

View File

@ -125,7 +125,7 @@ class ProcessInstanceService:
if next_task_trying_again is not None: if next_task_trying_again is not None:
process_instance_api.next_task = ( process_instance_api.next_task = (
ProcessInstanceService.spiff_task_to_api_task( ProcessInstanceService.spiff_task_to_api_task(
next_task_trying_again, add_docs_and_forms=True processor, next_task_trying_again, add_docs_and_forms=True
) )
) )
@ -281,7 +281,9 @@ class ProcessInstanceService:
@staticmethod @staticmethod
def spiff_task_to_api_task( def spiff_task_to_api_task(
spiff_task: SpiffTask, add_docs_and_forms: bool = False processor: ProcessInstanceProcessor,
spiff_task: SpiffTask,
add_docs_and_forms: bool = False,
) -> Task: ) -> Task:
"""Spiff_task_to_api_task.""" """Spiff_task_to_api_task."""
task_type = spiff_task.task_spec.spec_type task_type = spiff_task.task_spec.spec_type
@ -315,6 +317,8 @@ class ProcessInstanceService:
if spiff_task.parent: if spiff_task.parent:
parent_id = spiff_task.parent.id parent_id = spiff_task.parent.id
serialized_task_spec = processor.serialize_task_spec(spiff_task.task_spec)
task = Task( task = Task(
spiff_task.id, spiff_task.id,
spiff_task.task_spec.name, spiff_task.task_spec.name,
@ -328,6 +332,7 @@ class ProcessInstanceService:
process_identifier=spiff_task.task_spec._wf_spec.name, process_identifier=spiff_task.task_spec._wf_spec.name,
properties=props, properties=props,
parent=parent_id, parent=parent_id,
event_definition=serialized_task_spec.get("event_definition"),
call_activity_process_identifier=call_activity_process_identifier, call_activity_process_identifier=call_activity_process_identifier,
) )

View File

@ -0,0 +1,137 @@
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" 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:collaboration id="Collaboration_1022bxx">
<bpmn:participant id="Participant_1gfxnts" processRef="Process_1oafp0t" />
</bpmn:collaboration>
<bpmn:process id="Process_1oafp0t" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1l15rbh</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1l15rbh" sourceRef="StartEvent_1" targetRef="Gateway_0n53kj7" />
<bpmn:eventBasedGateway id="Gateway_0n53kj7">
<bpmn:incoming>Flow_1l15rbh</bpmn:incoming>
<bpmn:outgoing>Flow_0d35i06</bpmn:outgoing>
<bpmn:outgoing>Flow_0tzaigt</bpmn:outgoing>
<bpmn:outgoing>Flow_1vld4r2</bpmn:outgoing>
</bpmn:eventBasedGateway>
<bpmn:sequenceFlow id="Flow_0d35i06" sourceRef="Gateway_0n53kj7" targetRef="Event_0xbr8bu" />
<bpmn:intermediateCatchEvent id="Event_0xbr8bu">
<bpmn:incoming>Flow_0d35i06</bpmn:incoming>
<bpmn:outgoing>Flow_1w3n49n</bpmn:outgoing>
<bpmn:messageEventDefinition id="MessageEventDefinition_1aazu62" messageRef="message_1" />
</bpmn:intermediateCatchEvent>
<bpmn:intermediateCatchEvent id="Event_0himdx6">
<bpmn:incoming>Flow_0tzaigt</bpmn:incoming>
<bpmn:outgoing>Flow_1q47ol8</bpmn:outgoing>
<bpmn:messageEventDefinition id="MessageEventDefinition_0oersqt" messageRef="message_2" />
</bpmn:intermediateCatchEvent>
<bpmn:sequenceFlow id="Flow_0tzaigt" sourceRef="Gateway_0n53kj7" targetRef="Event_0himdx6" />
<bpmn:sequenceFlow id="Flow_1vld4r2" sourceRef="Gateway_0n53kj7" targetRef="Event_0e4owa3" />
<bpmn:sequenceFlow id="Flow_13ai5vv" sourceRef="Event_0e4owa3" targetRef="Activity_0uum4kq" />
<bpmn:endEvent id="Event_0vmxgb9">
<bpmn:incoming>Flow_1q47ol8</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1q47ol8" sourceRef="Event_0himdx6" targetRef="Event_0vmxgb9" />
<bpmn:sequenceFlow id="Flow_1w3n49n" sourceRef="Event_0xbr8bu" targetRef="Event_174a838" />
<bpmn:endEvent id="Event_174a838">
<bpmn:incoming>Flow_1w3n49n</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1vwnf3n" sourceRef="Activity_0uum4kq" targetRef="Event_1ixib8a" />
<bpmn:intermediateCatchEvent id="Event_0e4owa3">
<bpmn:incoming>Flow_1vld4r2</bpmn:incoming>
<bpmn:outgoing>Flow_13ai5vv</bpmn:outgoing>
<bpmn:timerEventDefinition id="TimerEventDefinition_1fnogr9">
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">timedelta(hours=1)</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>
<bpmn:manualTask id="Activity_0uum4kq" name="Any Task">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>Click the button.</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_13ai5vv</bpmn:incoming>
<bpmn:outgoing>Flow_1vwnf3n</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:endEvent id="Event_1ixib8a">
<bpmn:incoming>Flow_1vwnf3n</bpmn:incoming>
</bpmn:endEvent>
</bpmn:process>
<bpmn:message id="message_1" name="Message 1">
<bpmn:extensionElements>
<spiffworkflow:messageVariable>result</spiffworkflow:messageVariable>
</bpmn:extensionElements>
</bpmn:message>
<bpmn:message id="message_2" name="Message 2">
<bpmn:extensionElements>
<spiffworkflow:messageVariable>result</spiffworkflow:messageVariable>
</bpmn:extensionElements>
</bpmn:message>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_1022bxx">
<bpmndi:BPMNShape id="Participant_1gfxnts_di" bpmnElement="Participant_1gfxnts" isHorizontal="true">
<dc:Bounds x="120" y="70" width="630" height="310" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="192" y="172" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0yjcvjd_di" bpmnElement="Gateway_0n53kj7">
<dc:Bounds x="285" y="165" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0xbr8bu_di" bpmnElement="Event_0xbr8bu">
<dc:Bounds x="392" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0himdx6_di" bpmnElement="Event_0himdx6">
<dc:Bounds x="392" y="172" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0vmxgb9_di" bpmnElement="Event_0vmxgb9">
<dc:Bounds x="492" y="172" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_174a838_di" bpmnElement="Event_174a838">
<dc:Bounds x="492" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0e4owa3_di" bpmnElement="Event_0e4owa3">
<dc:Bounds x="392" y="272" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_104jmxm_di" bpmnElement="Activity_0uum4kq">
<dc:Bounds x="480" y="250" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1ixib8a_di" bpmnElement="Event_1ixib8a">
<dc:Bounds x="662" y="272" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1l15rbh_di" bpmnElement="Flow_1l15rbh">
<di:waypoint x="228" y="190" />
<di:waypoint x="285" y="190" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0d35i06_di" bpmnElement="Flow_0d35i06">
<di:waypoint x="310" y="165" />
<di:waypoint x="310" y="120" />
<di:waypoint x="392" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0tzaigt_di" bpmnElement="Flow_0tzaigt">
<di:waypoint x="335" y="190" />
<di:waypoint x="392" y="190" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1vld4r2_di" bpmnElement="Flow_1vld4r2">
<di:waypoint x="310" y="215" />
<di:waypoint x="310" y="290" />
<di:waypoint x="392" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_13ai5vv_di" bpmnElement="Flow_13ai5vv">
<di:waypoint x="428" y="290" />
<di:waypoint x="480" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1q47ol8_di" bpmnElement="Flow_1q47ol8">
<di:waypoint x="428" y="190" />
<di:waypoint x="492" y="190" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1w3n49n_di" bpmnElement="Flow_1w3n49n">
<di:waypoint x="428" y="120" />
<di:waypoint x="492" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1vwnf3n_di" bpmnElement="Flow_1vwnf3n">
<di:waypoint x="580" y="290" />
<di:waypoint x="662" y="290" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -4,6 +4,7 @@ import json
import os import os
import time import time
from typing import Any from typing import Any
from typing import Dict
import pytest import pytest
from flask.app import Flask from flask.app import Flask
@ -2537,6 +2538,147 @@ class TestProcessApi(BaseTest):
print("test_script_unit_test_run") print("test_script_unit_test_run")
def test_send_event(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_script_unit_test_run."""
process_group_id = "test_group"
process_model_id = "process_navigation"
bpmn_file_name = "process_navigation.bpmn"
bpmn_file_location = "process_navigation"
process_model_identifier = self.create_group_and_model_with_bpmn(
client=client,
user=with_super_admin_user,
process_group_id=process_group_id,
process_model_id=process_model_id,
bpmn_file_name=bpmn_file_name,
bpmn_file_location=bpmn_file_location,
)
bpmn_file_data_bytes = self.get_test_data_file_contents(
bpmn_file_name, bpmn_file_location
)
self.create_spec_file(
client=client,
process_model_id=process_model_identifier,
process_model_location=process_model_identifier,
file_name=bpmn_file_name,
file_data=bpmn_file_data_bytes,
user=with_super_admin_user,
)
headers = self.logged_in_headers(with_super_admin_user)
response = self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers
)
process_instance_id = response.json["id"]
client.post(
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
# This is exactly the same the test above, but some reason I to a totally irrelevant type.
data: Dict = {
"correlation_properties": [],
"expression": None,
"external": True,
"internal": False,
"payload": {"message": "message 1"},
"name": "Message 1",
"typename": "MessageEventDefinition",
}
response = client.post(
f"/v1.0/send-event/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
headers=self.logged_in_headers(with_super_admin_user),
content_type="application/json",
data=json.dumps(data),
)
assert response.json["status"] == "complete"
response = client.get(
f"/v1.0/task-data/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}?all_tasks=true",
headers=self.logged_in_headers(with_super_admin_user),
)
end = next(task for task in response.json if task["name"] == "End")
assert end["data"]["result"] == {"message": "message 1"}
def test_manual_complete_task(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_script_unit_test_run."""
process_group_id = "test_group"
process_model_id = "process_navigation"
bpmn_file_name = "process_navigation.bpmn"
bpmn_file_location = "process_navigation"
process_model_identifier = self.create_group_and_model_with_bpmn(
client=client,
user=with_super_admin_user,
process_group_id=process_group_id,
process_model_id=process_model_id,
bpmn_file_name=bpmn_file_name,
bpmn_file_location=bpmn_file_location,
)
bpmn_file_data_bytes = self.get_test_data_file_contents(
bpmn_file_name, bpmn_file_location
)
self.create_spec_file(
client=client,
process_model_id=process_model_identifier,
process_model_location=process_model_identifier,
file_name=bpmn_file_name,
file_data=bpmn_file_data_bytes,
user=with_super_admin_user,
)
headers = self.logged_in_headers(with_super_admin_user)
response = self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers
)
process_instance_id = response.json["id"]
client.post(
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
data = {
"dateTime": "timedelta(hours=1)",
"external": True,
"internal": True,
"label": "Event_0e4owa3",
"typename": "TimerEventDefinition",
}
response = client.post(
f"/v1.0/send-event/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
headers=self.logged_in_headers(with_super_admin_user),
content_type="application/json",
data=json.dumps(data),
)
response = client.get(
f"/v1.0/task-data/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert len(response.json) == 1
task = response.json[0]
response = client.post(
f"/v1.0/task-complete/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/{task['id']}",
headers=self.logged_in_headers(with_super_admin_user),
content_type="application/json",
)
assert response.json["status"] == "suspended"
def setup_initial_groups_for_move_tests( def setup_initial_groups_for_move_tests(
self, client: FlaskClient, with_super_admin_user: UserModel self, client: FlaskClient, with_super_admin_user: UserModel
) -> None: ) -> None:

View File

@ -16,7 +16,10 @@ export const useUriListForPermissions = () => {
processInstanceReportListPath: '/v1.0/process-instances/reports', processInstanceReportListPath: '/v1.0/process-instances/reports',
processInstanceResumePath: `/v1.0/process-instance-resume/${params.process_model_id}/${params.process_instance_id}`, processInstanceResumePath: `/v1.0/process-instance-resume/${params.process_model_id}/${params.process_instance_id}`,
processInstanceSuspendPath: `/v1.0/process-instance-suspend/${params.process_model_id}/${params.process_instance_id}`, processInstanceSuspendPath: `/v1.0/process-instance-suspend/${params.process_model_id}/${params.process_instance_id}`,
processInstanceResetPath: `/v1.0/process-instance-reset/${params.process_model_id}/${params.process_instance_id}`,
processInstanceTaskListDataPath: `/v1.0/task-data/${params.process_model_id}/${params.process_instance_id}`, processInstanceTaskListDataPath: `/v1.0/task-data/${params.process_model_id}/${params.process_instance_id}`,
processInstanceSendEventPath: `/v1.0/send-event/${params.process_model_id}/${params.process_instance_id}`,
processInstanceCompleteTaskPath: `/v1.0/complete-task/${params.process_model_id}/${params.process_instance_id}`,
processInstanceTaskListPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}/task-info`, processInstanceTaskListPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}/task-info`,
processInstanceTaskListForMePath: `/v1.0/process-instances/for-me/${params.process_model_id}/${params.process_instance_id}/task-info`, processInstanceTaskListForMePath: `/v1.0/process-instances/for-me/${params.process_model_id}/${params.process_instance_id}/task-info`,
processInstanceTerminatePath: `/v1.0/process-instance-terminate/${params.process_model_id}/${params.process_instance_id}`, processInstanceTerminatePath: `/v1.0/process-instance-terminate/${params.process_model_id}/${params.process_instance_id}`,

View File

@ -25,6 +25,7 @@ import {
ButtonSet, ButtonSet,
Tag, Tag,
Modal, Modal,
Dropdown,
Stack, Stack,
// @ts-ignore // @ts-ignore
} from '@carbon/react'; } from '@carbon/react';
@ -66,6 +67,11 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const [processDataToDisplay, setProcessDataToDisplay] = const [processDataToDisplay, setProcessDataToDisplay] =
useState<ProcessData | null>(null); useState<ProcessData | null>(null);
const [editingTaskData, setEditingTaskData] = useState<boolean>(false); const [editingTaskData, setEditingTaskData] = useState<boolean>(false);
const [selectingEvent, setSelectingEvent] = useState<boolean>(false);
const [eventToSend, setEventToSend] = useState<any>({});
const [eventPayload, setEventPayload] = useState<string>('{}');
const [eventTextEditorEnabled, setEventTextEditorEnabled] =
useState<boolean>(false);
const setErrorObject = (useContext as any)(ErrorContext)[1]; const setErrorObject = (useContext as any)(ErrorContext)[1];
@ -84,10 +90,13 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
[`${targetUris.processInstanceResumePath}`]: ['POST'], [`${targetUris.processInstanceResumePath}`]: ['POST'],
[`${targetUris.processInstanceSuspendPath}`]: ['POST'], [`${targetUris.processInstanceSuspendPath}`]: ['POST'],
[`${targetUris.processInstanceTerminatePath}`]: ['POST'], [`${targetUris.processInstanceTerminatePath}`]: ['POST'],
[targetUris.processInstanceResetPath]: ['POST'],
[targetUris.messageInstanceListPath]: ['GET'], [targetUris.messageInstanceListPath]: ['GET'],
[targetUris.processInstanceActionPath]: ['DELETE'], [targetUris.processInstanceActionPath]: ['DELETE'],
[targetUris.processInstanceLogListPath]: ['GET'], [targetUris.processInstanceLogListPath]: ['GET'],
[targetUris.processInstanceTaskListDataPath]: ['GET', 'PUT'], [targetUris.processInstanceTaskListDataPath]: ['GET', 'PUT'],
[targetUris.processInstanceSendEventPath]: ['POST'],
[targetUris.processInstanceCompleteTaskPath]: ['POST'],
[targetUris.processModelShowPath]: ['PUT'], [targetUris.processModelShowPath]: ['PUT'],
[taskListPath]: ['GET'], [taskListPath]: ['GET'],
}; };
@ -253,6 +262,14 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return spiffStepLink(<CaretRight />, 1); return spiffStepLink(<CaretRight />, 1);
}; };
const resetProcessInstance = () => {
HttpService.makeCallToBackend({
path: `${targetUris.processInstanceResetPath}/${currentSpiffStep()}`,
successCallback: refreshPage,
httpMethod: 'POST',
});
};
const getInfoTag = () => { const getInfoTag = () => {
if (!processInstance) { if (!processInstance) {
return null; return null;
@ -508,9 +525,62 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
); );
}; };
const cancelEditingTaskData = () => { const canSendEvent = (task: any) => {
// 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' &&
showingLastSpiffStep()
);
};
const canCompleteTask = (task: any) => {
return (
processInstance &&
processInstance.status === 'suspended' &&
ability.can('POST', targetUris.processInstanceCompleteTaskPath) &&
task.state === 'READY' &&
showingLastSpiffStep()
);
};
const canResetProcess = (task: any) => {
return (
ability.can('POST', targetUris.processInstanceResetPath) &&
processInstance &&
processInstance.status === 'suspended' &&
task.state === 'READY' &&
!showingLastSpiffStep()
);
};
const getEvents = (task: any) => {
const handleMessage = (eventDefinition: any) => {
if (eventDefinition.typename === 'MessageEventDefinition') {
const newEvent = eventDefinition;
delete newEvent.message_var;
newEvent.payload = {};
return newEvent;
}
return eventDefinition;
};
if (task.event_definition && task.event_definition.event_definitions)
return task.event_definition.event_definitions.map((e: any) =>
handleMessage(e)
);
if (task.event_definition) return [handleMessage(task.event_definition)];
return [];
};
const cancelUpdatingTask = () => {
setEditingTaskData(false); setEditingTaskData(false);
setSelectingEvent(false);
initializeTaskDataToDisplay(taskToDisplay); initializeTaskDataToDisplay(taskToDisplay);
setEventPayload('{}');
setErrorObject(null); setErrorObject(null);
}; };
@ -550,7 +620,30 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
}); });
}; };
const taskDataButtons = (task: any) => { const sendEvent = () => {
if ('payload' in eventToSend)
eventToSend.payload = JSON.parse(eventPayload);
HttpService.makeCallToBackend({
path: `/send-event/${modifiedProcessModelId}/${params.process_instance_id}`,
httpMethod: 'POST',
successCallback: saveTaskDataResult,
failureCallback: saveTaskDataFailure,
postBody: eventToSend,
});
};
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 },
});
};
const taskDisplayButtons = (task: any) => {
const buttons = []; const buttons = [];
if ( if (
@ -579,22 +672,36 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
); );
} }
if (canEditTaskData(task)) { if (editingTaskData) {
if (editingTaskData) { buttons.push(
buttons.push( <Button data-qa="save-task-data-button" onClick={saveTaskData}>
<Button data-qa="save-task-data-button" onClick={saveTaskData}> Save
Save </Button>
</Button> );
); buttons.push(
buttons.push( <Button
<Button data-qa="cancel-task-data-edit-button"
data-qa="cancel-task-data-edit-button" onClick={cancelUpdatingTask}
onClick={cancelEditingTaskData} >
> Cancel
Cancel </Button>
</Button> );
); } else if (selectingEvent) {
} else { buttons.push(
<Button data-qa="send-event-button" onClick={sendEvent}>
Send
</Button>
);
buttons.push(
<Button
data-qa="cancel-task-data-edit-button"
onClick={cancelUpdatingTask}
>
Cancel
</Button>
);
} else {
if (canEditTaskData(task)) {
buttons.push( buttons.push(
<Button <Button
data-qa="edit-task-data-button" data-qa="edit-task-data-button"
@ -604,6 +711,44 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
</Button> </Button>
); );
} }
if (canCompleteTask(task)) {
buttons.push(
<Button
data-qa="mark-task-complete-button"
onClick={() => completeTask(false)}
>
Mark Complete
</Button>
);
buttons.push(
<Button
data-qa="execute-task-complete-button"
onClick={() => completeTask(true)}
>
Execute Task
</Button>
);
}
if (canSendEvent(task)) {
buttons.push(
<Button
data-qa="select-event-button"
onClick={() => setSelectingEvent(true)}
>
Send Event
</Button>
);
}
if (canResetProcess(task)) {
buttons.push(
<Button
data-qa="reset-process-button"
onClick={() => resetProcessInstance()}
>
Resume Process Here
</Button>
);
}
} }
return buttons; return buttons;
@ -623,8 +768,42 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
); );
}; };
const taskDataDisplayArea = () => { const eventSelector = (candidateEvents: any) => {
const editor = (
<Editor
height={300}
width="auto"
defaultLanguage="json"
defaultValue={eventPayload}
onChange={(value: any) => setEventPayload(value || '{}')}
options={{ readOnly: !eventTextEditorEnabled }}
/>
);
return selectingEvent ? (
<Stack orientation="vertical">
<Dropdown
id="process-instance-select-event"
titleText="Event"
label="Select Event"
items={candidateEvents}
itemToString={(item: any) => item.name || item.label || item.typename}
onChange={(value: any) => {
setEventToSend(value.selectedItem);
setEventTextEditorEnabled(
value.selectedItem.typename === 'MessageEventDefinition'
);
}}
/>
{editor}
</Stack>
) : (
taskDataContainer()
);
};
const taskUpdateDisplayArea = () => {
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay }; const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay };
const candidateEvents: any = getEvents(taskToUse);
if (taskToDisplay) { if (taskToDisplay) {
return ( return (
<Modal <Modal
@ -634,9 +813,11 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
> >
<Stack orientation="horizontal" gap={2}> <Stack orientation="horizontal" gap={2}>
{taskToUse.name} ({taskToUse.type}): {taskToUse.state} {taskToUse.name} ({taskToUse.type}): {taskToUse.state}
{taskDataButtons(taskToUse)} {taskDisplayButtons(taskToUse)}
</Stack> </Stack>
{taskDataContainer()} {selectingEvent
? eventSelector(candidateEvents)
: taskDataContainer()}
</Modal> </Modal>
); );
} }
@ -723,7 +904,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
<br /> <br />
{getInfoTag()} {getInfoTag()}
<br /> <br />
{taskDataDisplayArea()} {taskUpdateDisplayArea()}
{processDataDisplayArea()} {processDataDisplayArea()}
{stepsElement()} {stepsElement()}
<br /> <br />