Merge remote-tracking branch 'origin/main' into feature/process_api_blueprint_refactor

This commit is contained in:
burnettk 2022-12-29 20:41:50 -05:00
commit d1e911950d
12 changed files with 798 additions and 28 deletions

View File

@ -2989,7 +2989,18 @@ psycopg2 = [
{file = "psycopg2-2.9.4.tar.gz", hash = "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f"}, {file = "psycopg2-2.9.4.tar.gz", hash = "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f"},
] ]
pyasn1 = [ pyasn1 = [
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
] ]
pycodestyle = [ pycodestyle = [

View File

@ -1049,6 +1049,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
@ -1472,6 +1505,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

@ -26,11 +26,15 @@ from spiffworkflow_backend.models.process_instance import (
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.spec_reference import SpecReferenceSchema from spiffworkflow_backend.models.spec_reference import SpecReferenceSchema
from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel
from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.git_service import GitService from spiffworkflow_backend.services.git_service import GitService
from spiffworkflow_backend.services.process_instance_processor import ( from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor, ProcessInstanceProcessor,
) )
from spiffworkflow_backend.services.process_instance_service import (
ProcessInstanceService,
)
from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.process_model_service import ProcessModelService
@ -87,6 +91,46 @@ def process_list() -> Any:
return SpecReferenceSchema(many=True).dump(references) return SpecReferenceSchema(many=True).dump(references)
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_data_show( def process_data_show(
process_instance_id: int, process_instance_id: int,
process_data_identifier: str, process_data_identifier: str,
@ -195,6 +239,107 @@ def _get_required_parameter_or_raise(parameter: str, post_body: dict[str, Any])
return return_value return return_value
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

@ -531,7 +531,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()
@ -552,7 +552,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)

View File

@ -158,7 +158,8 @@ 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

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,148 @@ 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),
)
assert response.status_code == 200
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 />