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 b2b3c0a45..94b9976c8 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py @@ -2,7 +2,10 @@ import base64 import hashlib import time +from datetime import datetime +from datetime import timezone from typing import Any +from typing import Dict from typing import Generator from typing import List from typing import Optional @@ -12,6 +15,7 @@ from urllib.parse import unquote import sentry_sdk from flask import current_app from flask import g +from SpiffWorkflow.bpmn.specs.events.event_definitions import TimerEventDefinition # type: ignore from SpiffWorkflow.bpmn.specs.events.IntermediateEvent import _BoundaryEventParent # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore @@ -86,6 +90,29 @@ class ProcessInstanceService: process_model = ProcessModelService.get_process_model(process_model_identifier) return cls.create_process_instance(process_model, user) + @classmethod + def waiting_event_can_be_skipped(cls, waiting_event: Dict[str, Any], now_in_utc: datetime) -> bool: + # + # over time this function can gain more knowledge of different event types, + # for now we are just handling Duration Timer events. + # + # example: {'event_type': 'Duration Timer', 'name': None, 'value': '2023-04-27T20:15:10.626656+00:00'} + # + event_type = waiting_event.get("event_type") + if event_type == "Duration Timer": + event_value = waiting_event.get("value") + if event_value is not None: + event_datetime = TimerEventDefinition.get_datetime(event_value) + return event_datetime > now_in_utc # type: ignore + return False + + @classmethod + def all_waiting_events_can_be_skipped(cls, waiting_events: List[Dict[str, Any]]) -> bool: + for waiting_event in waiting_events: + if not cls.waiting_event_can_be_skipped(waiting_event, datetime.now(timezone.utc)): + return False + return True + @classmethod def ready_user_task_has_associated_timer(cls, processor: ProcessInstanceProcessor) -> bool: for ready_user_task in processor.bpmn_process_instance.get_ready_user_tasks(): @@ -101,7 +128,10 @@ class ProcessInstanceService: if processor.process_instance_model.status != status_value: return True - return status_value == "user_input_required" and not cls.ready_user_task_has_associated_timer(processor) + if status_value == "user_input_required" and cls.ready_user_task_has_associated_timer(processor): + return cls.all_waiting_events_can_be_skipped(processor.bpmn_process_instance.waiting_events()) + + return False @classmethod def do_waiting(cls, status_value: str = ProcessInstanceStatus.waiting.value) -> None: diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py index 0c27a5383..9d25adda1 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py @@ -1,4 +1,6 @@ """Test_process_instance_processor.""" +from datetime import datetime +from datetime import timezone from typing import Optional from flask.app import Flask @@ -213,3 +215,33 @@ class TestProcessInstanceService(BaseTest): assert len(models) == 2 self._check_sample_file_data_model("File", 0, models[0]) self._check_sample_file_data_model("File", 1, models[1]) + + def test_does_not_skip_events_it_does_not_know_about(self) -> None: + assert not ( + ProcessInstanceService.waiting_event_can_be_skipped( + {"event_type": "Unknown", "name": None, "value": "2023-04-27T20:15:10.626656+00:00"}, + datetime.now(timezone.utc), + ) + ) + + def test_does_skip_duration_timer_events_for_the_future(self) -> None: + assert ProcessInstanceService.waiting_event_can_be_skipped( + {"event_type": "Duration Timer", "name": None, "value": "2023-04-27T20:15:10.626656+00:00"}, + datetime.fromisoformat("2023-04-26T20:15:10.626656+00:00"), + ) + + def test_does_not_skip_duration_timer_events_for_the_past(self) -> None: + assert not ( + ProcessInstanceService.waiting_event_can_be_skipped( + {"event_type": "Duration Timer", "name": None, "value": "2023-04-27T20:15:10.626656+00:00"}, + datetime.fromisoformat("2023-04-28T20:15:10.626656+00:00"), + ) + ) + + def test_does_not_skip_duration_timer_events_for_now(self) -> None: + assert not ( + ProcessInstanceService.waiting_event_can_be_skipped( + {"event_type": "Duration Timer", "name": None, "value": "2023-04-27T20:15:10.626656+00:00"}, + datetime.fromisoformat("2023-04-27T20:15:10.626656+00:00"), + ) + )