From db57a7ab78c0133a146eb5bf099b9a3f9e5d2508 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Wed, 14 Dec 2022 15:21:39 -0500 Subject: [PATCH 01/15] 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 +- .../src/routes/ProcessInstanceShow.tsx | 108 +++++++++++++++++- 6 files changed, 171 insertions(+), 12 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index e7dc00fe2..bc8bab525 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py index 52bb11715..be3a3e685 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index 739e689d2..5eba99988 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index d1df67428..5b2fc88d6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py index f98eaae18..4f1f60eea 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py +++ b/spiffworkflow-backend/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 diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index c407c7713..2e0a7ef37 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -20,6 +20,7 @@ import { ButtonSet, Tag, Modal, + Dropdown, Stack, // @ts-ignore } from '@carbon/react'; @@ -47,6 +48,9 @@ export default function ProcessInstanceShow() { const [taskToDisplay, setTaskToDisplay] = useState(null); const [taskDataToDisplay, setTaskDataToDisplay] = useState(''); const [editingTaskData, setEditingTaskData] = useState(false); + const [selectingEvent, setSelectingEvent] = useState(false); + const [eventToSend, setEventToSend] = useState({}); + const [eventPayload, setEventPayload] = useState('{}'); const setErrorMessage = (useContext as any)(ErrorContext)[1]; @@ -404,8 +408,34 @@ export default function ProcessInstanceShow() { ); }; - const cancelEditingTaskData = () => { + const canSendEvent = (task: any) => { + // We actually could allow this for any waiting events + const taskTypes = ['Event Based Gateway']; + return ( + taskTypes.filter((t) => t === task.type).length > 0 && + task.state === 'WAITING' && + showingLastSpiffStep(processInstance as any) + ); + }; + + const getEvents = (task: any) => { + const handleMessage = (eventDefinition: any) => { + if (eventDefinition.typename === 'MessageEventDefinition') { + delete eventDefinition.message_var; + eventDefinition.payload = {}; + } + 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); + setSelectingEvent(false); initializeTaskDataToDisplay(taskToDisplay); setErrorMessage(null); }; @@ -446,6 +476,18 @@ export default function ProcessInstanceShow() { }); }; + const sendEvent = () => { + if ('payload' in eventToSend) + eventToSend.payload = JSON.parse(eventPayload); + HttpService.makeCallToBackend({ + path: `/process-instances/${params.process_instance_id}/event`, + httpMethod: 'POST', + successCallback: saveTaskDataResult, + failureCallback: saveTaskDataFailure, + postBody: eventToSend, + }); + }; + const taskDataButtons = (task: any) => { const buttons = []; @@ -460,7 +502,7 @@ export default function ProcessInstanceShow() { ); } - if (canEditTaskData(task)) { + if (canEditTaskData(task) || canSendEvent(task)) { if (editingTaskData) { buttons.push( + ); + } else if (selectingEvent) { + buttons.push( + + ); + buttons.push( + @@ -487,6 +546,16 @@ export default function ProcessInstanceShow() { Edit ); + if (canSendEvent(task)) { + buttons.push( + + ); + } } } @@ -507,8 +576,35 @@ export default function ProcessInstanceShow() { ); }; - const taskDataDisplayArea = () => { + const eventSelector = (candidateEvents: any) => { + const editor = ( + setEventPayload(value || '{}')} + /> + ) + return selectingEvent ? ( + + item.name || item.label || item.typename} + onChange={(value: any) => setEventToSend(value.selectedItem)} + /> + {'payload' in eventToSend ? editor : ''} + + ) : ( + taskDataContainer() + ); + }; + + const taskUpdateDisplayArea = () => { const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay }; + const candidateEvents: any = getEvents(taskToUse); if (taskToDisplay) { return ( - {taskDataContainer()} + {selectingEvent ? eventSelector(candidateEvents) : taskDataContainer()} ); } @@ -592,7 +688,7 @@ export default function ProcessInstanceShow() {
{getInfoTag(processInstanceToUse)}
- {taskDataDisplayArea()} + {taskUpdateDisplayArea()} {stepsElement(processInstanceToUse)}
Date: Wed, 14 Dec 2022 16:13:28 -0500 Subject: [PATCH 02/15] fix event UI --- .../src/routes/ProcessInstanceShow.tsx | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index 2e0a7ef37..3710cee02 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -51,6 +51,8 @@ export default function ProcessInstanceShow() { const [selectingEvent, setSelectingEvent] = useState(false); const [eventToSend, setEventToSend] = useState({}); const [eventPayload, setEventPayload] = useState('{}'); + const [eventTextEditorEnabled, setEventTextEditorEnabled] = + useState(false); const setErrorMessage = (useContext as any)(ErrorContext)[1]; @@ -413,7 +415,7 @@ export default function ProcessInstanceShow() { const taskTypes = ['Event Based Gateway']; return ( taskTypes.filter((t) => t === task.type).length > 0 && - task.state === 'WAITING' && + task.state === 'WAITING' && showingLastSpiffStep(processInstance as any) ); }; @@ -421,15 +423,18 @@ export default function ProcessInstanceShow() { const getEvents = (task: any) => { const handleMessage = (eventDefinition: any) => { if (eventDefinition.typename === 'MessageEventDefinition') { - delete eventDefinition.message_var; - eventDefinition.payload = {}; + 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 task.event_definition.event_definitions.map((e: any) => + handleMessage(e) + ); + if (task.event_definition) return [handleMessage(task.event_definition)]; return []; }; @@ -437,6 +442,7 @@ export default function ProcessInstanceShow() { setEditingTaskData(false); setSelectingEvent(false); initializeTaskDataToDisplay(taskToDisplay); + setEventPayload('{}'); setErrorMessage(null); }; @@ -522,10 +528,7 @@ export default function ProcessInstanceShow() { ); } else if (selectingEvent) { buttons.push( - ); @@ -582,9 +585,11 @@ export default function ProcessInstanceShow() { height={300} width="auto" defaultLanguage="json" + defaultValue={eventPayload} onChange={(value: any) => setEventPayload(value || '{}')} + options={{ readOnly: !eventTextEditorEnabled }} /> - ) + ); return selectingEvent ? ( item.name || item.label || item.typename} - onChange={(value: any) => setEventToSend(value.selectedItem)} + onChange={(value: any) => { + setEventToSend(value.selectedItem); + setEventTextEditorEnabled( + value.selectedItem.typename === 'MessageEventDefinition' + ); + }} /> - {'payload' in eventToSend ? editor : ''} + {editor} ) : ( taskDataContainer() @@ -616,7 +626,9 @@ export default function ProcessInstanceShow() { {taskToUse.name} ({taskToUse.type}): {taskToUse.state} {taskDataButtons(taskToUse)} - {selectingEvent ? eventSelector(candidateEvents) : taskDataContainer()} + {selectingEvent + ? eventSelector(candidateEvents) + : taskDataContainer()} ); } From b1e1a01785ce625ddba07b6bdd16fbc37c5cafca Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Thu, 15 Dec 2022 11:39:28 -0500 Subject: [PATCH 03/15] reformat & fix types --- .../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/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index c0a84c52c..d27805d6d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index adff3bbfc..f06d0c17d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py index adee870fc..a83b998f8 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py +++ b/spiffworkflow-backend/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 e23a5be1523363e3e639e7604e641aec407076b9 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Fri, 16 Dec 2022 16:22:59 -0500 Subject: [PATCH 04/15] update url to allow permissions on send event --- spiffworkflow-backend/src/spiffworkflow_backend/api.yml | 2 +- spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index e725d2b76..afea32c42 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/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 diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index e69b4854d..73b95020f 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -528,7 +528,7 @@ export default function ProcessInstanceShow() { if ('payload' in eventToSend) eventToSend.payload = JSON.parse(eventPayload); HttpService.makeCallToBackend({ - path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}/event`, + path: `/send-event/${modifiedProcessModelId}/${params.process_instance_id}`, httpMethod: 'POST', successCallback: saveTaskDataResult, failureCallback: saveTaskDataFailure, From 54426b19bf908a4965cba8153f3be0dde3972d79 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Sun, 18 Dec 2022 10:44:42 -0500 Subject: [PATCH 05/15] allow marking task complete without executing --- .../src/spiffworkflow_backend/api.yml | 87 +++++++++++++------ .../routes/process_api_blueprint.py | 24 +++++ .../services/process_instance_processor.py | 14 +++ .../src/routes/ProcessInstanceShow.tsx | 25 +++++- 4 files changed, 120 insertions(+), 30 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 6def7f2cd..0249f930e 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index fe178c961..af92bb324 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index f06d0c17d..31d42773d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/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.""" diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index 6df404ec9..8c625d2a9 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -546,6 +546,16 @@ export default function ProcessInstanceShow() { }); }; + const markTaskComplete = () => { + const taskToUse: any = taskToDisplay; + HttpService.makeCallToBackend({ + path: `/task-complete/${modifiedProcessModelId}/${params.process_instance_id}/${taskToUse.id}`, + httpMethod: 'POST', + successCallback: saveTaskDataResult, + failureCallback: saveTaskDataFailure, + }); + }; + const taskDataButtons = (task: any) => { const buttons = []; @@ -584,7 +594,7 @@ export default function ProcessInstanceShow() { ); buttons.push( ); @@ -616,12 +626,21 @@ export default function ProcessInstanceShow() { if (canSendEvent(task)) { buttons.push( ); + } else { + buttons.push( + + ); } } } From 8b863c1a1eb61b3c9a5ae1dc6c56845d0f583c3e Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Mon, 19 Dec 2022 11:33:48 -0500 Subject: [PATCH 06/15] 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 spiffworkflow-backend/tests/data/process_navigation/process_navigation.bpmn diff --git a/spiffworkflow-backend/tests/data/process_navigation/process_navigation.bpmn b/spiffworkflow-backend/tests/data/process_navigation/process_navigation.bpmn new file mode 100644 index 000000000..9f2f26bf4 --- /dev/null +++ b/spiffworkflow-backend/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/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index 3bc21456e..145082858 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/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 36924b8410642665ea7b331869759b67df7a547e Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Mon, 19 Dec 2022 14:01:52 -0500 Subject: [PATCH 07/15] fix typing problem --- .../spiffworkflow_backend/integration/test_process_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index 145082858..87139f0be 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/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 c219b057c7344b3c6b5a30c002396174e5edcbf4 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Tue, 20 Dec 2022 11:05:46 -0500 Subject: [PATCH 08/15] 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/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index 3e83cb385..d01468e8f 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index ea7100f2b..301461b72 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/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 9f19c4694589165a59d168b7e36f0d97ebc3e616 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Tue, 20 Dec 2022 11:10:59 -0500 Subject: [PATCH 09/15] fix more bs errors --- .../spiffworkflow_backend/routes/process_api_blueprint.py | 4 ++-- .../services/process_instance_processor.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index d01468e8f..023bc0d31 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index 301461b72..87d5e1030 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/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 4a6b219220ddfdf44130466062313589a751c738 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Tue, 20 Dec 2022 11:19:27 -0500 Subject: [PATCH 10/15] reformat file again --- .../services/process_instance_processor.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index 87d5e1030..1921a2035 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/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 d4faa160befb356b08130d02b6583ab060184423 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Thu, 29 Dec 2022 13:55:19 -0500 Subject: [PATCH 11/15] fix show previous spiff steps --- .../src/spiffworkflow_backend/routes/process_api_blueprint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index c566cfd8e..ee3e4c623 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/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 0edb8904708a84e453fcc7b2bdf1d21a6b0f7520 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Thu, 29 Dec 2022 15:26:29 -0500 Subject: [PATCH 12/15] add endpoint to reset process to earlier step --- .../src/spiffworkflow_backend/api.yml | 33 ++++++++++++++++ .../routes/process_api_blueprint.py | 39 +++++++++++++++++++ .../src/hooks/UriListForPermissions.tsx | 1 + .../src/routes/ProcessInstanceShow.tsx | 32 ++++++++++++++- 4 files changed, 103 insertions(+), 2 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index ea23d2fe2..2a2478635 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index ee3e4c623..b8db2dabb 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/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, diff --git a/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx b/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx index c9ef03e0a..f8e5f07f8 100644 --- a/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx +++ b/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx @@ -16,6 +16,7 @@ export const useUriListForPermissions = () => { processInstanceReportListPath: '/v1.0/process-instances/reports', 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}`, + 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}`, 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}`, diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index 03f8bd673..d984b6648 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -90,6 +90,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { [`${targetUris.processInstanceResumePath}`]: ['POST'], [`${targetUris.processInstanceSuspendPath}`]: ['POST'], [`${targetUris.processInstanceTerminatePath}`]: ['POST'], + [targetUris.processInstanceResetPath]: ['POST'], [targetUris.messageInstanceListPath]: ['GET'], [targetUris.processInstanceActionPath]: ['DELETE'], [targetUris.processInstanceLogListPath]: ['GET'], @@ -261,6 +262,14 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { return spiffStepLink(, 1); }; + const resetProcessInstance = () => { + HttpService.makeCallToBackend({ + path: `${targetUris.processInstanceResetPath}/${currentSpiffStep()}`, + successCallback: refreshPage, + httpMethod: 'POST', + }); + }; + const getInfoTag = () => { if (!processInstance) { return null; @@ -535,6 +544,15 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { ); }; + const canResetProcess = (task: any) => { + return ( + processInstance && + processInstance.status === 'suspended' && + task.state === 'READY' && + !showingLastSpiffStep() + ); + }; + const getEvents = (task: any) => { const handleMessage = (eventDefinition: any) => { if (eventDefinition.typename === 'MessageEventDefinition') { @@ -619,7 +637,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { }); }; - const taskDataButtons = (task: any) => { + const taskDisplayButtons = (task: any) => { const buttons = []; if ( @@ -707,6 +725,16 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { ); } + if (canResetProcess(task)) { + buttons.push( + + ); + } } return buttons; @@ -771,7 +799,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { > {taskToUse.name} ({taskToUse.type}): {taskToUse.state} - {taskDataButtons(taskToUse)} + {taskDisplayButtons(taskToUse)} {selectingEvent ? eventSelector(candidateEvents) From 72100e6d929342b78e35ea7d98b99aa292b84bfc Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Thu, 29 Dec 2022 16:34:38 -0500 Subject: [PATCH 13/15] 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 +++++++++++++------ .../src/routes/ProcessInstanceShow.tsx | 22 ++++++++++--- 4 files changed, 49 insertions(+), 24 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 2a2478635..304fa4e19 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index b8db2dabb..2b1f13062 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index 79de71f00..cd838d793 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/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: diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index d984b6648..678ebdf2a 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -529,6 +529,8 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { // 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' && @@ -536,8 +538,10 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { ); }; - const canMarkTaskComplete = (task: any) => { + const canCompleteTask = (task: any) => { return ( + processInstance && + processInstance.status === 'suspended' && ability.can('POST', targetUris.processInstanceCompleteTaskPath) && task.state === 'READY' && showingLastSpiffStep() @@ -546,6 +550,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { const canResetProcess = (task: any) => { return ( + ability.can('POST', targetUris.processInstanceResetPath) && processInstance && processInstance.status === 'suspended' && task.state === 'READY' && @@ -627,13 +632,14 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { }); }; - const markTaskComplete = () => { + 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 }, }); }; @@ -705,15 +711,23 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { ); } - if (canMarkTaskComplete(task)) { + if (canCompleteTask(task)) { buttons.push( ); + buttons.push( + + ); } if (canSendEvent(task)) { buttons.push( From 9ae1de7627181b9c3f5efc1543381d7f1c95326d Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Thu, 29 Dec 2022 17:51:43 -0500 Subject: [PATCH 14/15] fix test --- .../tests/spiffworkflow_backend/integration/test_process_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index 4c6364740..77c466449 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/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 7a4519b1776ccc00f8b10fe392a94ef635c5c789 Mon Sep 17 00:00:00 2001 From: burnettk Date: Thu, 29 Dec 2022 18:26:43 -0500 Subject: [PATCH 15/15] 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/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index 2b1f13062..192d0f5ab 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/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/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index cd838d793..bc31b1d79 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/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