progress-page-display-error (#1859)
* display the given when a task fails while on the progress page * add task info to the error details on progress page and added foreign key from event table to task table w/ burnettk * do not attempt to add error details if one cannot be found w/ burnettk * delete pi events when tasks are deleted w/ burnettk * fixed migration file w/ burnettk * removed db migration changes w/ burnettk * pyl w/ burnettk --------- Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
parent
a038d544a9
commit
c11e9ba1b4
|
@ -10,6 +10,7 @@ from sqlalchemy.orm import validates
|
|||
from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum
|
||||
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
|
||||
from spiffworkflow_backend.models.db import db
|
||||
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
|
||||
|
||||
|
@ -21,6 +22,7 @@ class ProcessInstanceEventType(SpiffEnum):
|
|||
process_instance_resumed = "process_instance_resumed"
|
||||
process_instance_rewound_to_task = "process_instance_rewound_to_task"
|
||||
process_instance_suspended = "process_instance_suspended"
|
||||
process_instance_suspended_for_error = "process_instance_suspended_for_error"
|
||||
process_instance_terminated = "process_instance_terminated"
|
||||
task_cancelled = "task_cancelled"
|
||||
task_completed = "task_completed"
|
||||
|
@ -36,6 +38,8 @@ class ProcessInstanceEventModel(SpiffworkflowBaseDBModel):
|
|||
id: int = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# use task guid so we can bulk insert without worrying about whether or not the task has an id yet
|
||||
# we considered putting a foreign key constraint on this in july 2024, and decided not to mostly
|
||||
# because it was scary. it would also delete events on reset and migrate, which felt less than ideal.
|
||||
task_guid: str | None = db.Column(db.String(36), nullable=True, index=True)
|
||||
process_instance_id: int = db.Column(ForeignKey("process_instance.id"), nullable=False, index=True)
|
||||
|
||||
|
@ -49,6 +53,10 @@ class ProcessInstanceEventModel(SpiffworkflowBaseDBModel):
|
|||
"ProcessInstanceMigrationDetailModel", back_populates="process_instance_event", cascade="delete"
|
||||
) # type: ignore
|
||||
|
||||
def task(self) -> TaskModel | None:
|
||||
task_model: TaskModel | None = TaskModel.query.filter_by(guid=self.task_guid).first()
|
||||
return task_model
|
||||
|
||||
@validates("event_type")
|
||||
def validate_event_type(self, key: str, value: Any) -> Any:
|
||||
return self.validate_enum_field(key, value, ProcessInstanceEventType)
|
||||
|
|
|
@ -36,6 +36,8 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
|||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceTaskDataCannotBeUpdatedError
|
||||
from spiffworkflow_backend.models.process_instance_error_detail import ProcessInstanceErrorDetailModel
|
||||
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel
|
||||
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType
|
||||
from spiffworkflow_backend.models.task import Task
|
||||
from spiffworkflow_backend.models.task import TaskModel
|
||||
|
@ -447,7 +449,7 @@ def task_submit(
|
|||
def process_instance_progress(
|
||||
process_instance_id: int,
|
||||
) -> flask.wrappers.Response:
|
||||
response: dict[str, Task | ProcessInstanceModel | list] = {}
|
||||
response: dict[str, Task | ProcessInstanceModel | list | dict[str, str]] = {}
|
||||
process_instance = _find_process_instance_for_me_or_raise(process_instance_id, include_actions=True)
|
||||
|
||||
principal = _find_principal_or_raise()
|
||||
|
@ -461,6 +463,34 @@ def process_instance_progress(
|
|||
# any time we assign this process_instance, the frontend progress page will redirect to process instance show
|
||||
response["process_instance"] = process_instance
|
||||
|
||||
# look for the most recent error event for this instance
|
||||
if process_instance.status in [ProcessInstanceStatus.error.value, ProcessInstanceStatus.suspended.value]:
|
||||
pi_error_details = (
|
||||
ProcessInstanceErrorDetailModel.query.join(
|
||||
ProcessInstanceEventModel,
|
||||
ProcessInstanceErrorDetailModel.process_instance_event_id == ProcessInstanceEventModel.id,
|
||||
)
|
||||
.filter(
|
||||
ProcessInstanceEventModel.process_instance_id == process_instance.id,
|
||||
ProcessInstanceEventModel.event_type.in_( # type: ignore
|
||||
[
|
||||
ProcessInstanceEventType.process_instance_error.value,
|
||||
ProcessInstanceEventType.task_failed.value,
|
||||
]
|
||||
),
|
||||
)
|
||||
.order_by(ProcessInstanceEventModel.timestamp.desc()) # type: ignore
|
||||
.first()
|
||||
)
|
||||
if pi_error_details is not None:
|
||||
response["error_details"] = pi_error_details
|
||||
task_model = pi_error_details.process_instance_event.task()
|
||||
if task_model is not None:
|
||||
response["process_instance_event"] = {
|
||||
"task_definition_identifier": task_model.task_definition.bpmn_identifier,
|
||||
"task_definition_name": task_model.task_definition.bpmn_name,
|
||||
}
|
||||
|
||||
user_instructions = TaskInstructionsForEndUserModel.retrieve_and_clear(process_instance.id)
|
||||
response["instructions"] = user_instructions
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ from spiffworkflow_backend.exceptions.process_entity_not_found_error import Proc
|
|||
from spiffworkflow_backend.models.db import db
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
|
||||
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType
|
||||
from spiffworkflow_backend.services.process_instance_tmp_service import ProcessInstanceTmpService
|
||||
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
||||
|
||||
|
||||
|
@ -57,8 +59,9 @@ class ErrorHandlingService:
|
|||
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
@classmethod
|
||||
def _handle_system_notification(
|
||||
cls,
|
||||
error: Exception,
|
||||
process_instance: ProcessInstanceModel,
|
||||
exception_notification_addresses: list,
|
||||
|
@ -94,7 +97,11 @@ class ErrorHandlingService:
|
|||
db.session.commit()
|
||||
MessageService.correlate_send_message(message_instance)
|
||||
|
||||
@staticmethod
|
||||
def _set_instance_status(process_instance: ProcessInstanceModel, status: str) -> None:
|
||||
@classmethod
|
||||
def _set_instance_status(cls, process_instance: ProcessInstanceModel, status: str) -> None:
|
||||
process_instance.status = status
|
||||
db.session.add(process_instance)
|
||||
if status == ProcessInstanceStatus.suspended.value:
|
||||
ProcessInstanceTmpService.add_event_to_process_instance(
|
||||
process_instance, ProcessInstanceEventType.process_instance_suspended_for_error.value, add_to_db_session=True
|
||||
)
|
||||
|
|
|
@ -133,7 +133,11 @@ class ProcessInstanceQueueService:
|
|||
ProcessInstanceTmpService.add_event_to_process_instance(
|
||||
process_instance, ProcessInstanceEventType.process_instance_error.value, exception=ex
|
||||
)
|
||||
ErrorHandlingService.handle_error(process_instance, ex)
|
||||
|
||||
# we call dequeued multiple times but we want this code to only happen once.
|
||||
# assume that if we are not reentering_lock then this is the top level call and should be the one to handle the error.
|
||||
if not reentering_lock:
|
||||
ErrorHandlingService.handle_error(process_instance, ex)
|
||||
raise ex
|
||||
finally:
|
||||
if not reentering_lock:
|
||||
|
|
|
@ -12,7 +12,10 @@ import { HUMAN_TASK_TYPES, refreshAtInterval } from '../helpers';
|
|||
import HttpService from '../services/HttpService';
|
||||
import DateAndTimeService from '../services/DateAndTimeService';
|
||||
import InstructionsForEndUser from './InstructionsForEndUser';
|
||||
import { ErrorDisplayStateless } from './ErrorDisplay';
|
||||
import {
|
||||
ErrorDisplayStateless,
|
||||
errorForDisplayFromProcessInstanceErrorDetail,
|
||||
} from './ErrorDisplay';
|
||||
|
||||
type OwnProps = {
|
||||
processInstanceId: number;
|
||||
|
@ -60,6 +63,12 @@ export default function ProcessInstanceProgress({
|
|||
if (result.task && shouldRedirectToTask(result.task)) {
|
||||
// if task you can complete, go there
|
||||
navigate(`/tasks/${result.task.process_instance_id}/${result.task.id}`);
|
||||
} else if (result.error_details && result.process_instance_event) {
|
||||
const error = errorForDisplayFromProcessInstanceErrorDetail(
|
||||
result.process_instance_event,
|
||||
result.error_details,
|
||||
);
|
||||
stopRefreshing(error);
|
||||
} else if (result.process_instance) {
|
||||
// there is nothing super exciting happening right now. go to process instance.
|
||||
if (
|
||||
|
|
|
@ -503,8 +503,10 @@ export interface TaskInstructionForEndUser {
|
|||
}
|
||||
|
||||
export interface ProcessInstanceProgressResponse {
|
||||
error_details?: ProcessInstanceEventErrorDetail;
|
||||
instructions: TaskInstructionForEndUser[];
|
||||
process_instance?: ProcessInstance;
|
||||
process_instance_event?: ProcessInstanceLogEntry;
|
||||
task?: ProcessInstanceTask;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue