From 6c8cbe6f29a42a9940e9ac5f28951e408df582eb Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Wed, 14 Dec 2022 15:21:39 -0500 Subject: [PATCH 01/14] working but barely functional UI for manually sending events --- src/spiffworkflow_backend/api.yml | 21 ++++++++++++++++ src/spiffworkflow_backend/models/task.py | 4 ++++ .../routes/process_api_blueprint.py | 24 +++++++++++++++++-- .../services/process_instance_processor.py | 17 +++++++++++-- .../services/process_instance_service.py | 9 +++++-- 5 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/spiffworkflow_backend/api.yml b/src/spiffworkflow_backend/api.yml index e7dc00fe..bc8bab52 100755 --- a/src/spiffworkflow_backend/api.yml +++ b/src/spiffworkflow_backend/api.yml @@ -586,6 +586,27 @@ paths: schema: $ref: "#/components/schemas/Workflow" + /process-instances/{process_instance_id}/event: + parameters: + - 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" + /process-models/{process_group_id}/{process_model_id}/script-unit-tests: parameters: - name: process_group_id diff --git a/src/spiffworkflow_backend/models/task.py b/src/spiffworkflow_backend/models/task.py index 52bb1171..be3a3e68 100644 --- a/src/spiffworkflow_backend/models/task.py +++ b/src/spiffworkflow_backend/models/task.py @@ -118,6 +118,7 @@ class Task: form_schema: Union[str, None] = None, form_ui_schema: Union[str, None] = None, parent: Optional[str] = None, + event_definition: Union[dict[str, Any], None] = None ): """__init__.""" self.id = id @@ -129,6 +130,7 @@ class Task: self.documentation = documentation self.lane = lane self.parent = parent + self.event_definition = event_definition self.data = data if self.data is None: @@ -187,6 +189,7 @@ class Task: "form_schema": self.form_schema, "form_ui_schema": self.form_ui_schema, "parent": self.parent, + "event_definition": self.event_definition, } @classmethod @@ -287,6 +290,7 @@ class TaskSchema(Schema): "process_instance_id", "form_schema", "form_ui_schema", + "event_definition", ] multi_instance_type = EnumField(MultiInstanceType) diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index 739e689d..5eba9998 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -1311,7 +1311,7 @@ def process_instance_task_list( 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) task.data = spiff_task.data tasks.append(task) @@ -1344,7 +1344,9 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response form_schema_file_name = properties["formJsonSchemaFilename"] if "formUiSchemaFilename" in properties: 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.process_model_display_name = process_model.display_name task.process_model_identifier = process_model.id @@ -1882,3 +1884,21 @@ def update_task_data(process_instance_id: str, task_id: str, body: Dict) -> Resp status=200, mimetype="application/json", ) + +def send_bpmn_event(process_instance_id: str, body: Dict) -> Response: + 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", + ) diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index d1df6742..5b2fc88d 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -45,9 +45,8 @@ from SpiffWorkflow.spiff.serializer.task_spec_converters import ( from SpiffWorkflow.spiff.serializer.task_spec_converters import EndEventConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import ( IntermediateCatchEventConverter, -) -from SpiffWorkflow.spiff.serializer.task_spec_converters import ( IntermediateThrowEventConverter, + EventBasedGatewayConverter, ) from SpiffWorkflow.spiff.serializer.task_spec_converters import ManualTaskConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import NoneTaskConverter @@ -254,6 +253,7 @@ class ProcessInstanceProcessor: EndEventConverter, IntermediateCatchEventConverter, IntermediateThrowEventConverter, + EventBasedGatewayConverter, ManualTaskConverter, NoneTaskConverter, ReceiveTaskConverter, @@ -267,6 +267,7 @@ class ProcessInstanceProcessor: ] ) _serializer = BpmnWorkflowSerializer(wf_spec_converter, version=SERIALIZER_VERSION) + _event_serializer = EventBasedGatewayConverter() PROCESS_INSTANCE_ID_KEY = "process_instance_id" VALIDATION_PROCESS_KEY = "validate_only" @@ -658,6 +659,18 @@ class ProcessInstanceProcessor: db.session.delete(at) db.session.commit() + def serialize_task_spec(self, task_spec: SpiffTask) -> dict[str, Any]: + return self._serializer.spec_converter.convert(task_spec) + + def send_bpmn_event(self, event_data: dict[str,Any]) -> None: + 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) + @staticmethod def get_parser() -> MyCustomParser: """Get_parser.""" diff --git a/src/spiffworkflow_backend/services/process_instance_service.py b/src/spiffworkflow_backend/services/process_instance_service.py index f98eaae1..4f1f60ee 100644 --- a/src/spiffworkflow_backend/services/process_instance_service.py +++ b/src/spiffworkflow_backend/services/process_instance_service.py @@ -121,7 +121,7 @@ class ProcessInstanceService: if next_task_trying_again is not None: process_instance_api.next_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 ) ) @@ -277,7 +277,9 @@ class ProcessInstanceService: @staticmethod 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: """Spiff_task_to_api_task.""" task_type = spiff_task.task_spec.spec_type @@ -306,6 +308,8 @@ class ProcessInstanceService: if spiff_task.parent: parent_id = spiff_task.parent.id + serialized_task_spec = processor.serialize_task_spec(spiff_task.task_spec) + task = Task( spiff_task.id, spiff_task.task_spec.name, @@ -319,6 +323,7 @@ class ProcessInstanceService: process_name=spiff_task.task_spec._wf_spec.description, properties=props, parent=parent_id, + event_definition=serialized_task_spec.get("event_definition"), ) return task From e10cb73f7032bb55a8f5c828d422ea7fff41401d Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Thu, 15 Dec 2022 11:39:28 -0500 Subject: [PATCH 02/14] reformat & fix types --- src/spiffworkflow_backend/routes/process_api_blueprint.py | 1 + .../services/process_instance_processor.py | 8 +++++--- .../services/process_instance_service.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index c0a84c52..d27805d6 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -2041,6 +2041,7 @@ def update_task_data( mimetype="application/json", ) + def send_bpmn_event(process_instance_id: str, body: Dict) -> Response: process_instance = ProcessInstanceModel.query.filter( ProcessInstanceModel.id == int(process_instance_id) diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index adff3bbf..f06d0c17 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -705,15 +705,17 @@ class ProcessInstanceProcessor: db.session.delete(at) db.session.commit() - def serialize_task_spec(self, task_spec: SpiffTask) -> dict[str, Any]: + def serialize_task_spec(self, task_spec: SpiffTask) -> Any: return self._serializer.spec_converter.convert(task_spec) - def send_bpmn_event(self, event_data: dict[str,Any]) -> None: + def send_bpmn_event(self, event_data: dict[str, Any]) -> None: 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}") + 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) diff --git a/src/spiffworkflow_backend/services/process_instance_service.py b/src/spiffworkflow_backend/services/process_instance_service.py index adee870f..a83b998f 100644 --- a/src/spiffworkflow_backend/services/process_instance_service.py +++ b/src/spiffworkflow_backend/services/process_instance_service.py @@ -279,7 +279,7 @@ class ProcessInstanceService: def spiff_task_to_api_task( processor: ProcessInstanceProcessor, spiff_task: SpiffTask, - add_docs_and_forms: bool = False + add_docs_and_forms: bool = False, ) -> Task: """Spiff_task_to_api_task.""" task_type = spiff_task.task_spec.spec_type From 10c92cd58662c6be6039ae53256f2525556833dc Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Fri, 16 Dec 2022 16:22:59 -0500 Subject: [PATCH 03/14] update url to allow permissions on send event --- src/spiffworkflow_backend/api.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spiffworkflow_backend/api.yml b/src/spiffworkflow_backend/api.yml index e725d2b7..afea32c4 100755 --- a/src/spiffworkflow_backend/api.yml +++ b/src/spiffworkflow_backend/api.yml @@ -808,7 +808,7 @@ paths: schema: $ref: "#/components/schemas/OkTrue" - /process-instances/{modified_process_model_identifier}/{process_instance_id}/event: + /send-event/{modified_process_model_identifier}/{process_instance_id}: parameters: - name: modified_process_model_identifier in: path From 71a154db1e46bbb72c78fb373a76a5d6bb3b7fe3 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Sun, 18 Dec 2022 10:44:42 -0500 Subject: [PATCH 04/14] allow marking task complete without executing --- src/spiffworkflow_backend/api.yml | 87 +++++++++++++------ .../routes/process_api_blueprint.py | 24 +++++ .../services/process_instance_processor.py | 14 +++ 3 files changed, 98 insertions(+), 27 deletions(-) diff --git a/src/spiffworkflow_backend/api.yml b/src/spiffworkflow_backend/api.yml index 6def7f2c..0249f930 100755 --- a/src/spiffworkflow_backend/api.yml +++ b/src/spiffworkflow_backend/api.yml @@ -861,33 +861,6 @@ paths: schema: $ref: "#/components/schemas/OkTrue" - /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" - /process-instances/reports: parameters: - name: page @@ -1279,6 +1252,66 @@ paths: schema: $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.mark_task_complete + 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: get: tags: diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index fe178c96..af92bb32 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -2170,6 +2170,30 @@ def send_bpmn_event( ) +def mark_task_complete( + modified_process_model_identifier: str, + process_instance_id: str, + task_id: str, + body: Dict, +) -> Response: + process_instance = ProcessInstanceModel.query.filter( + ProcessInstanceModel.id == int(process_instance_id) + ).first() + if process_instance: + processor = ProcessInstanceProcessor(process_instance) + processor.mark_task_complete(task_id) + else: + raise ApiError( + error_code="send_bpmn_event_error", + message=f"Could not skip 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: """Commit_and_push_to_git.""" if current_app.config["GIT_COMMIT_ON_SAVE"]: diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index f06d0c17..31d42773 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -17,6 +17,7 @@ from typing import Optional from typing import Tuple from typing import TypedDict from typing import Union +from uuid import UUID import dateparser import pytz @@ -706,6 +707,8 @@ class ProcessInstanceProcessor: db.session.commit() def serialize_task_spec(self, task_spec: SpiffTask) -> Any: + # 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: @@ -719,6 +722,17 @@ class ProcessInstanceProcessor: self.bpmn_process_instance.catch(event_definition) self.do_engine_steps(save=True) + def mark_task_complete(self, task_id: str) -> None: + spiff_task = self.bpmn_process_instance.get_task(UUID(task_id)) + spiff_task._set_state(TaskState.COMPLETED) + self.bpmn_process_instance.last_task = spiff_task + for child in spiff_task.children: + child.task_spec._update(child) + current_app.logger.info( + f"Task {spiff_task.task_spec.name} of process instance {self.process_instance_model.id} skipped" + ) + self.do_engine_steps(save=True) + @staticmethod def get_parser() -> MyCustomParser: """Get_parser.""" From a7fb08b11dbb5d5d8532919823042f3531978278 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Mon, 19 Dec 2022 11:33:48 -0500 Subject: [PATCH 05/14] add integration tests for process nav endpoints --- .../process_navigation.bpmn | 137 +++++++++++++++++ .../integration/test_process_api.py | 140 ++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 tests/data/process_navigation/process_navigation.bpmn diff --git a/tests/data/process_navigation/process_navigation.bpmn b/tests/data/process_navigation/process_navigation.bpmn new file mode 100644 index 00000000..9f2f26bf --- /dev/null +++ b/tests/data/process_navigation/process_navigation.bpmn @@ -0,0 +1,137 @@ + + + + + + + + Flow_1l15rbh + + + + Flow_1l15rbh + Flow_0d35i06 + Flow_0tzaigt + Flow_1vld4r2 + + + + Flow_0d35i06 + Flow_1w3n49n + + + + Flow_0tzaigt + Flow_1q47ol8 + + + + + + + Flow_1q47ol8 + + + + + Flow_1w3n49n + + + + Flow_1vld4r2 + Flow_13ai5vv + + timedelta(hours=1) + + + + + Click the button. + + Flow_13ai5vv + Flow_1vwnf3n + + + Flow_1vwnf3n + + + + + result + + + + + result + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/spiffworkflow_backend/integration/test_process_api.py b/tests/spiffworkflow_backend/integration/test_process_api.py index 3bc21456..14508285 100644 --- a/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/tests/spiffworkflow_backend/integration/test_process_api.py @@ -2497,6 +2497,146 @@ class TestProcessApi(BaseTest): 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( + 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 = { + "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( + 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"] == "complete" + def setup_initial_groups_for_move_tests( self, client: FlaskClient, with_super_admin_user: UserModel ) -> None: From bd835f980933c0c22c43ee4bd387625994632978 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Mon, 19 Dec 2022 14:01:52 -0500 Subject: [PATCH 06/14] fix typing problem --- tests/spiffworkflow_backend/integration/test_process_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/spiffworkflow_backend/integration/test_process_api.py b/tests/spiffworkflow_backend/integration/test_process_api.py index 14508285..87139f0b 100644 --- a/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/tests/spiffworkflow_backend/integration/test_process_api.py @@ -4,6 +4,7 @@ import json import os import time from typing import Any +from typing import Dict import pytest from flask.app import Flask @@ -2541,7 +2542,8 @@ class TestProcessApi(BaseTest): headers=self.logged_in_headers(with_super_admin_user), ) - data = { + # 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, From e1d132cf6806757e7e4f8f6f1927c75f17861c2a Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Tue, 20 Dec 2022 11:05:46 -0500 Subject: [PATCH 07/14] fix lint errors --- .../routes/process_api_blueprint.py | 2 ++ .../services/process_instance_processor.py | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index 3e83cb38..d01468e8 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -2290,6 +2290,7 @@ def send_bpmn_event( 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() @@ -2314,6 +2315,7 @@ def mark_task_complete( task_id: str, body: Dict, ) -> Response: + """Mark a task complete without executing it""" process_instance = ProcessInstanceModel.query.filter( ProcessInstanceModel.id == int(process_instance_id) ).first() diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index ea7100f2..301461b7 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -44,11 +44,9 @@ from SpiffWorkflow.spiff.serializer.task_spec_converters import ( CallActivityTaskConverter, ) from SpiffWorkflow.spiff.serializer.task_spec_converters import EndEventConverter -from SpiffWorkflow.spiff.serializer.task_spec_converters import ( - IntermediateCatchEventConverter, - IntermediateThrowEventConverter, - EventBasedGatewayConverter, -) +from SpiffWorkflow.spiff.serializer.task_spec_converters import EventBasedGatewayConverter +from SpiffWorkflow.spiff.serializer.task_spec_converters import IntermediateCatchEventConverter +from SpiffWorkflow.spiff.serializer.task_spec_converters import IntermediateThrowEventConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import ManualTaskConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import NoneTaskConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import ReceiveTaskConverter @@ -64,7 +62,6 @@ from SpiffWorkflow.spiff.serializer.task_spec_converters import UserTaskConverte from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import TaskState from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore - from spiffworkflow_backend.models.file import File from spiffworkflow_backend.models.file import FileType from spiffworkflow_backend.models.group import GroupModel @@ -708,11 +705,13 @@ class ProcessInstanceProcessor: 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: @@ -724,6 +723,7 @@ class ProcessInstanceProcessor: self.do_engine_steps(save=True) def mark_task_complete(self, task_id: str) -> None: + """Mark the task complete without executing it""" spiff_task = self.bpmn_process_instance.get_task(UUID(task_id)) spiff_task._set_state(TaskState.COMPLETED) self.bpmn_process_instance.last_task = spiff_task From e0ce9f71ede49f1dea63478cefdf9cbb585c4803 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Tue, 20 Dec 2022 11:10:59 -0500 Subject: [PATCH 08/14] fix more bs errors --- src/spiffworkflow_backend/routes/process_api_blueprint.py | 4 ++-- .../services/process_instance_processor.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index d01468e8..023bc0d3 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -2290,7 +2290,7 @@ def send_bpmn_event( process_instance_id: str, body: Dict, ) -> Response: - """Send a bpmn event to a workflow""" + """Send a bpmn event to a workflow.""" process_instance = ProcessInstanceModel.query.filter( ProcessInstanceModel.id == int(process_instance_id) ).first() @@ -2315,7 +2315,7 @@ def mark_task_complete( task_id: str, body: Dict, ) -> Response: - """Mark a task complete without executing it""" + """Mark a task complete without executing it.""" process_instance = ProcessInstanceModel.query.filter( ProcessInstanceModel.id == int(process_instance_id) ).first() diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index 301461b7..87d5e103 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -705,13 +705,13 @@ class ProcessInstanceProcessor: db.session.commit() def serialize_task_spec(self, task_spec: SpiffTask) -> Any: - """Get a serialized version of a task spec""" + """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""" + """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: @@ -723,7 +723,7 @@ class ProcessInstanceProcessor: self.do_engine_steps(save=True) def mark_task_complete(self, task_id: str) -> None: - """Mark the task complete without executing it""" + """Mark the task complete without executing it.""" spiff_task = self.bpmn_process_instance.get_task(UUID(task_id)) spiff_task._set_state(TaskState.COMPLETED) self.bpmn_process_instance.last_task = spiff_task From 6cb0b068b4d4ca883472aeb0df41e295e3aeced0 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Tue, 20 Dec 2022 11:19:27 -0500 Subject: [PATCH 09/14] reformat file again --- .../services/process_instance_processor.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index 87d5e103..1921a203 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -44,9 +44,15 @@ from SpiffWorkflow.spiff.serializer.task_spec_converters import ( CallActivityTaskConverter, ) 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 IntermediateCatchEventConverter -from SpiffWorkflow.spiff.serializer.task_spec_converters import IntermediateThrowEventConverter +from SpiffWorkflow.spiff.serializer.task_spec_converters import ( + EventBasedGatewayConverter, +) +from SpiffWorkflow.spiff.serializer.task_spec_converters import ( + IntermediateCatchEventConverter, +) +from SpiffWorkflow.spiff.serializer.task_spec_converters import ( + IntermediateThrowEventConverter, +) from SpiffWorkflow.spiff.serializer.task_spec_converters import ManualTaskConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import NoneTaskConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import ReceiveTaskConverter From 76c71a7e592919ae82d8aee34309dd08fdb71029 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Thu, 29 Dec 2022 13:55:19 -0500 Subject: [PATCH 10/14] fix show previous spiff steps --- src/spiffworkflow_backend/routes/process_api_blueprint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index c566cfd8..ee3e4c62 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -1402,7 +1402,7 @@ def process_instance_task_list( step_detail = ( db.session.query(SpiffStepDetailsModel) .filter( - SpiffStepDetailsModel.process_instance.id == process_instance.id, + SpiffStepDetailsModel.process_instance_id == process_instance.id, SpiffStepDetailsModel.spiff_step == spiff_step, ) .first() From 6b78eddc82780891fab69295d31616f154c2982f Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Thu, 29 Dec 2022 15:26:29 -0500 Subject: [PATCH 11/14] add endpoint to reset process to earlier step --- src/spiffworkflow_backend/api.yml | 33 ++++++++++++++++ .../routes/process_api_blueprint.py | 39 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/spiffworkflow_backend/api.yml b/src/spiffworkflow_backend/api.yml index ea23d2fe..2a247863 100755 --- a/src/spiffworkflow_backend/api.yml +++ b/src/spiffworkflow_backend/api.yml @@ -1060,6 +1060,39 @@ paths: schema: $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: parameters: - name: page diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index ee3e4c62..b8db2dab 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -651,6 +651,45 @@ def process_instance_resume( 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 = 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( modified_process_model_identifier: str, process_instance_id: int, From 524434a10ef88e739849e10972dad14a2b9d5558 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Thu, 29 Dec 2022 16:34:38 -0500 Subject: [PATCH 12/14] allow option to complete single tasks with or without execution --- src/spiffworkflow_backend/api.yml | 2 +- .../routes/process_api_blueprint.py | 18 +++++------ .../services/process_instance_processor.py | 31 +++++++++++++------ 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/spiffworkflow_backend/api.yml b/src/spiffworkflow_backend/api.yml index 2a247863..304fa4e1 100755 --- a/src/spiffworkflow_backend/api.yml +++ b/src/spiffworkflow_backend/api.yml @@ -1565,7 +1565,7 @@ paths: schema: type: string post: - operationId: spiffworkflow_backend.routes.process_api_blueprint.mark_task_complete + operationId: spiffworkflow_backend.routes.process_api_blueprint.manual_complete_task summary: Mark a task complete without executing it tags: - Process Instances diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index b8db2dab..2b1f1306 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -28,11 +28,6 @@ from lxml import etree # type: ignore from lxml.builder import ElementMaker # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import TaskState -from sqlalchemy import and_ -from sqlalchemy import asc -from sqlalchemy import desc -from sqlalchemy import or_ - from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( ProcessEntityNotFoundError, ) @@ -99,6 +94,10 @@ from spiffworkflow_backend.services.secret_service import SecretService from spiffworkflow_backend.services.service_task_service import ServiceTaskService from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.user_service import UserService +from sqlalchemy import and_ +from sqlalchemy import asc +from sqlalchemy import desc +from sqlalchemy import or_ class TaskDataSelectOption(TypedDict): @@ -2189,23 +2188,24 @@ def send_bpmn_event( ) -def mark_task_complete( +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.mark_task_complete(task_id) + processor.manual_complete_task(task_id, execute) else: raise ApiError( - error_code="send_bpmn_event_error", - message=f"Could not skip Task {task_id} in Instance {process_instance_id}", + 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)), diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index 79de71f0..cd838d79 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -621,7 +621,7 @@ class ProcessInstanceProcessor: db.session.add(pim) db.session.commit() - def save(self) -> None: + def _save(self) -> None: """Saves the current state of this processor to the database.""" self.process_instance_model.bpmn_json = self.serialize() @@ -643,6 +643,9 @@ class ProcessInstanceProcessor: db.session.add(self.process_instance_model) 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( process_instance_id=self.process_instance_model.id ).all() @@ -729,17 +732,25 @@ class ProcessInstanceProcessor: self.bpmn_process_instance.catch(event_definition) self.do_engine_steps(save=True) - def mark_task_complete(self, task_id: str) -> None: - """Mark the task complete without executing it.""" + 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)) - spiff_task._set_state(TaskState.COMPLETED) + 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 - for child in spiff_task.children: - child.task_spec._update(child) - current_app.logger.info( - f"Task {spiff_task.task_spec.name} of process instance {self.process_instance_model.id} skipped" - ) - self.do_engine_steps(save=True) + self._save() + # Saving the workflow seems to reset the status + self.suspend() @staticmethod def get_parser() -> MyCustomParser: From e0f7da69ab191022331c1593bd09cb7f2db84471 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Thu, 29 Dec 2022 17:51:43 -0500 Subject: [PATCH 13/14] fix test --- tests/spiffworkflow_backend/integration/test_process_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/spiffworkflow_backend/integration/test_process_api.py b/tests/spiffworkflow_backend/integration/test_process_api.py index 4c636474..77c46644 100644 --- a/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/tests/spiffworkflow_backend/integration/test_process_api.py @@ -2677,7 +2677,7 @@ class TestProcessApi(BaseTest): headers=self.logged_in_headers(with_super_admin_user), content_type="application/json", ) - assert response.json["status"] == "complete" + assert response.json["status"] == "suspended" def setup_initial_groups_for_move_tests( self, client: FlaskClient, with_super_admin_user: UserModel From 22cdc28a8311d2479eb00fcb24e002ba7dc29117 Mon Sep 17 00:00:00 2001 From: burnettk Date: Thu, 29 Dec 2022 18:26:43 -0500 Subject: [PATCH 14/14] run ./bin/run_pyl --- .../routes/process_api_blueprint.py | 10 ++++++---- .../services/process_instance_processor.py | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index 2b1f1306..192d0f5a 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -28,6 +28,11 @@ from lxml import etree # type: ignore from lxml.builder import ElementMaker # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import TaskState +from sqlalchemy import and_ +from sqlalchemy import asc +from sqlalchemy import desc +from sqlalchemy import or_ + from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( ProcessEntityNotFoundError, ) @@ -94,10 +99,6 @@ from spiffworkflow_backend.services.secret_service import SecretService from spiffworkflow_backend.services.service_task_service import ServiceTaskService from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.user_service import UserService -from sqlalchemy import and_ -from sqlalchemy import asc -from sqlalchemy import desc -from sqlalchemy import or_ class TaskDataSelectOption(TypedDict): @@ -655,6 +656,7 @@ def process_instance_reset( modified_process_model_identifier: str, spiff_step: int = 0, ) -> flask.wrappers.Response: + """Process_instance_reset.""" process_instance = ProcessInstanceService().get_process_instance( process_instance_id ) diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index cd838d79..bc31b1d7 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -68,6 +68,7 @@ from SpiffWorkflow.spiff.serializer.task_spec_converters import UserTaskConverte from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import TaskState from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore + from spiffworkflow_backend.models.file import File from spiffworkflow_backend.models.file import FileType from spiffworkflow_backend.models.group import GroupModel