Merge pull request #102 from sartography/feature/waku-fault-message
Feature/waku fault message
This commit is contained in:
commit
5d5f1e777c
|
@ -77,3 +77,7 @@ SPIFF_DATABASE_TYPE = environ.get(
|
||||||
SPIFFWORKFLOW_BACKEND_DATABASE_URI = environ.get(
|
SPIFFWORKFLOW_BACKEND_DATABASE_URI = environ.get(
|
||||||
"SPIFFWORKFLOW_BACKEND_DATABASE_URI", default=None
|
"SPIFFWORKFLOW_BACKEND_DATABASE_URI", default=None
|
||||||
)
|
)
|
||||||
|
SYSTEM_NOTIFICATION_PROCESS_MODEL_MESSAGE_ID = environ.get(
|
||||||
|
"SYSTEM_NOTIFICATION_PROCESS_MODEL_MESSAGE_ID",
|
||||||
|
default="Message_SystemMessageNotification",
|
||||||
|
)
|
||||||
|
|
|
@ -56,6 +56,8 @@ def process_model_create(
|
||||||
"primary_process_id",
|
"primary_process_id",
|
||||||
"description",
|
"description",
|
||||||
"metadata_extraction_paths",
|
"metadata_extraction_paths",
|
||||||
|
"fault_or_suspend_on_exception",
|
||||||
|
"exception_notification_addresses",
|
||||||
]
|
]
|
||||||
body_filtered = {
|
body_filtered = {
|
||||||
include_item: body[include_item]
|
include_item: body[include_item]
|
||||||
|
@ -108,6 +110,8 @@ def process_model_update(
|
||||||
"primary_process_id",
|
"primary_process_id",
|
||||||
"description",
|
"description",
|
||||||
"metadata_extraction_paths",
|
"metadata_extraction_paths",
|
||||||
|
"fault_or_suspend_on_exception",
|
||||||
|
"exception_notification_addresses",
|
||||||
]
|
]
|
||||||
body_filtered = {
|
body_filtered = {
|
||||||
include_item: body[include_item]
|
include_item: body[include_item]
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
"""Error_handling_service."""
|
"""Error_handling_service."""
|
||||||
from typing import Any
|
import json
|
||||||
from typing import List
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
from flask import g
|
||||||
|
from flask.wrappers import Response
|
||||||
from flask_bpmn.api.api_error import ApiError
|
from flask_bpmn.api.api_error import ApiError
|
||||||
from flask_bpmn.models.db import db
|
from flask_bpmn.models.db import db
|
||||||
|
|
||||||
|
from spiffworkflow_backend.models.message_model import MessageModel
|
||||||
|
from spiffworkflow_backend.models.message_triggerable_process_model import (
|
||||||
|
MessageTriggerableProcessModel,
|
||||||
|
)
|
||||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
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 ProcessInstanceStatus
|
||||||
from spiffworkflow_backend.services.email_service import EmailService
|
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
||||||
|
from spiffworkflow_backend.services.message_service import MessageService
|
||||||
from spiffworkflow_backend.services.process_instance_processor import (
|
from spiffworkflow_backend.services.process_instance_processor import (
|
||||||
ProcessInstanceProcessor,
|
ProcessInstanceProcessor,
|
||||||
)
|
)
|
||||||
|
@ -38,6 +45,7 @@ class ErrorHandlingService:
|
||||||
process_model = ProcessModelService.get_process_model(
|
process_model = ProcessModelService.get_process_model(
|
||||||
_processor.process_model_identifier
|
_processor.process_model_identifier
|
||||||
)
|
)
|
||||||
|
# First, suspend or fault the instance
|
||||||
if process_model.fault_or_suspend_on_exception == "suspend":
|
if process_model.fault_or_suspend_on_exception == "suspend":
|
||||||
self.set_instance_status(
|
self.set_instance_status(
|
||||||
_processor.process_instance_model.id,
|
_processor.process_instance_model.id,
|
||||||
|
@ -50,57 +58,93 @@ class ErrorHandlingService:
|
||||||
ProcessInstanceStatus.error.value,
|
ProcessInstanceStatus.error.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Second, call the System Notification Process
|
||||||
|
# Note that this isn't the best way to do this.
|
||||||
|
# The configs are all in the model.
|
||||||
|
# Maybe we can move some of this to the notification process, or dmn tables.
|
||||||
if len(process_model.exception_notification_addresses) > 0:
|
if len(process_model.exception_notification_addresses) > 0:
|
||||||
try:
|
try:
|
||||||
# some notification method (waku?)
|
self.handle_system_notification(_error, process_model)
|
||||||
self.handle_email_notification(
|
|
||||||
_processor, _error, process_model.exception_notification_addresses
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# hmm... what to do if a notification method fails. Probably log, at least
|
# hmm... what to do if a notification method fails. Probably log, at least
|
||||||
current_app.logger.error(e)
|
current_app.logger.error(e)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def hanle_sentry_notification(_error: ApiError, _recipients: List) -> None:
|
def handle_system_notification(
|
||||||
"""SentryHandler."""
|
error: Union[ApiError, Exception], process_model: ProcessModelInfo
|
||||||
...
|
) -> Response:
|
||||||
|
"""Handle_system_notification."""
|
||||||
@staticmethod
|
recipients = process_model.exception_notification_addresses
|
||||||
def handle_email_notification(
|
message_text = (
|
||||||
processor: ProcessInstanceProcessor,
|
f"There was an exception running process {process_model.id}.\nOriginal"
|
||||||
error: Union[ApiError, Exception],
|
f" Error:\n{error.__repr__()}"
|
||||||
recipients: List,
|
)
|
||||||
) -> None:
|
message_payload = {"message_text": message_text, "recipients": recipients}
|
||||||
"""EmailHandler."""
|
message_identifier = current_app.config[
|
||||||
subject = "Unexpected error in app"
|
"SYSTEM_NOTIFICATION_PROCESS_MODEL_MESSAGE_ID"
|
||||||
if isinstance(error, ApiError):
|
]
|
||||||
content = f"{error.message}"
|
message_model = MessageModel.query.filter_by(
|
||||||
else:
|
identifier=message_identifier
|
||||||
content = str(error)
|
).first()
|
||||||
content_html = content
|
message_triggerable_process_model = (
|
||||||
|
MessageTriggerableProcessModel.query.filter_by(
|
||||||
EmailService.add_email(
|
message_model_id=message_model.id
|
||||||
subject,
|
).first()
|
||||||
"sender@company.com",
|
)
|
||||||
recipients,
|
process_instance = MessageService.process_message_triggerable_process_model(
|
||||||
content,
|
message_triggerable_process_model,
|
||||||
content_html,
|
message_identifier,
|
||||||
cc=None,
|
message_payload,
|
||||||
bcc=None,
|
g.user,
|
||||||
reply_to=None,
|
|
||||||
attachment_files=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
return Response(
|
||||||
def handle_waku_notification(_error: ApiError, _recipients: List) -> Any:
|
json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
|
||||||
"""WakuHandler."""
|
status=200,
|
||||||
# class WakuMessage:
|
mimetype="application/json",
|
||||||
# """WakuMessage."""
|
)
|
||||||
#
|
|
||||||
# payload: str
|
# @staticmethod
|
||||||
# contentTopic: str # Optional
|
# def handle_sentry_notification(_error: ApiError, _recipients: List) -> None:
|
||||||
# version: int # Optional
|
# """SentryHandler."""
|
||||||
# timestamp: int # Optional
|
# ...
|
||||||
|
#
|
||||||
|
# @staticmethod
|
||||||
|
# def handle_email_notification(
|
||||||
|
# processor: ProcessInstanceProcessor,
|
||||||
|
# error: Union[ApiError, Exception],
|
||||||
|
# recipients: List,
|
||||||
|
# ) -> None:
|
||||||
|
# """EmailHandler."""
|
||||||
|
# subject = "Unexpected error in app"
|
||||||
|
# if isinstance(error, ApiError):
|
||||||
|
# content = f"{error.message}"
|
||||||
|
# else:
|
||||||
|
# content = str(error)
|
||||||
|
# content_html = content
|
||||||
|
#
|
||||||
|
# EmailService.add_email(
|
||||||
|
# subject,
|
||||||
|
# "sender@company.com",
|
||||||
|
# recipients,
|
||||||
|
# content,
|
||||||
|
# content_html,
|
||||||
|
# cc=None,
|
||||||
|
# bcc=None,
|
||||||
|
# reply_to=None,
|
||||||
|
# attachment_files=None,
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# @staticmethod
|
||||||
|
# def handle_waku_notification(_error: ApiError, _recipients: List) -> Any:
|
||||||
|
# """WakuHandler."""
|
||||||
|
# # class WakuMessage:
|
||||||
|
# # """WakuMessage."""
|
||||||
|
# #
|
||||||
|
# # payload: str
|
||||||
|
# # contentTopic: str # Optional
|
||||||
|
# # version: int # Optional
|
||||||
|
# # timestamp: int # Optional
|
||||||
|
|
||||||
|
|
||||||
class FailingService:
|
class FailingService:
|
||||||
|
|
|
@ -2160,59 +2160,10 @@ class TestProcessApi(BaseTest):
|
||||||
assert process is not None
|
assert process is not None
|
||||||
assert process.status == "suspended"
|
assert process.status == "suspended"
|
||||||
|
|
||||||
def test_error_handler_with_email(
|
def test_error_handler_system_notification(self) -> None:
|
||||||
self,
|
"""Test_error_handler_system_notification."""
|
||||||
app: Flask,
|
# TODO: make sure the system notification process is run on exceptions
|
||||||
client: FlaskClient,
|
...
|
||||||
with_db_and_bpmn_file_cleanup: None,
|
|
||||||
with_super_admin_user: UserModel,
|
|
||||||
) -> None:
|
|
||||||
"""Test_error_handler."""
|
|
||||||
process_group_id = "data"
|
|
||||||
process_model_id = "error"
|
|
||||||
bpmn_file_name = "error.bpmn"
|
|
||||||
bpmn_file_location = "error"
|
|
||||||
process_model_identifier = self.create_group_and_model_with_bpmn(
|
|
||||||
client,
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
process_instance_id = self.setup_testing_instance(
|
|
||||||
client, process_model_identifier, with_super_admin_user
|
|
||||||
)
|
|
||||||
|
|
||||||
process_model = ProcessModelService.get_process_model(process_model_identifier)
|
|
||||||
ProcessModelService.update_process_model(
|
|
||||||
process_model,
|
|
||||||
{"exception_notification_addresses": ["with_super_admin_user@example.com"]},
|
|
||||||
)
|
|
||||||
|
|
||||||
mail = app.config["MAIL_APP"]
|
|
||||||
with mail.record_messages() as outbox:
|
|
||||||
response = 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),
|
|
||||||
)
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert len(outbox) == 1
|
|
||||||
message = outbox[0]
|
|
||||||
assert message.subject == "Unexpected error in app"
|
|
||||||
assert (
|
|
||||||
message.body == 'TypeError:can only concatenate str (not "int") to str'
|
|
||||||
)
|
|
||||||
assert message.recipients == process_model.exception_notification_addresses
|
|
||||||
|
|
||||||
process = (
|
|
||||||
db.session.query(ProcessInstanceModel)
|
|
||||||
.filter(ProcessInstanceModel.id == process_instance_id)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
assert process is not None
|
|
||||||
assert process.status == "error"
|
|
||||||
|
|
||||||
def test_task_data_is_set_even_if_process_instance_errors(
|
def test_task_data_is_set_even_if_process_instance_errors(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -8,6 +8,8 @@ import {
|
||||||
TextInput,
|
TextInput,
|
||||||
Grid,
|
Grid,
|
||||||
Column,
|
Column,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
} from '@carbon/react';
|
} from '@carbon/react';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -76,6 +78,9 @@ export default function ProcessModelForm({
|
||||||
display_name: processModel.display_name,
|
display_name: processModel.display_name,
|
||||||
description: processModel.description,
|
description: processModel.description,
|
||||||
metadata_extraction_paths: processModel.metadata_extraction_paths,
|
metadata_extraction_paths: processModel.metadata_extraction_paths,
|
||||||
|
fault_or_suspend_on_exception: processModel.fault_or_suspend_on_exception,
|
||||||
|
exception_notification_addresses:
|
||||||
|
processModel.exception_notification_addresses,
|
||||||
};
|
};
|
||||||
if (mode === 'new') {
|
if (mode === 'new') {
|
||||||
Object.assign(postBody, {
|
Object.assign(postBody, {
|
||||||
|
@ -173,6 +178,69 @@ export default function ProcessModelForm({
|
||||||
updateProcessModel({ metadata_extraction_paths: cep });
|
updateProcessModel({ metadata_extraction_paths: cep });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const notificationAddressForm = (
|
||||||
|
index: number,
|
||||||
|
notificationAddress: string
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<Grid>
|
||||||
|
<Column md={3} lg={7} sm={1}>
|
||||||
|
<TextInput
|
||||||
|
id={`process-model-notification-address-key-${index}`}
|
||||||
|
labelText="Address"
|
||||||
|
value={notificationAddress}
|
||||||
|
onChange={(event: any) => {
|
||||||
|
const notificationAddresses: string[] =
|
||||||
|
processModel.exception_notification_addresses || [];
|
||||||
|
notificationAddresses[index] = event.target.value;
|
||||||
|
updateProcessModel({
|
||||||
|
exception_notification_addresses: notificationAddresses,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
<Column md={1} lg={1} sm={1}>
|
||||||
|
<Button
|
||||||
|
kind="ghost"
|
||||||
|
renderIcon={TrashCan}
|
||||||
|
iconDescription="Remove Address"
|
||||||
|
hasIconOnly
|
||||||
|
size="lg"
|
||||||
|
className="with-extra-top-margin"
|
||||||
|
onClick={() => {
|
||||||
|
const notificationAddresses: string[] =
|
||||||
|
processModel.exception_notification_addresses || [];
|
||||||
|
notificationAddresses.splice(index, 1);
|
||||||
|
updateProcessModel({
|
||||||
|
exception_notification_addresses: notificationAddresses,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationAddressFormArea = () => {
|
||||||
|
if (processModel.exception_notification_addresses) {
|
||||||
|
return processModel.exception_notification_addresses.map(
|
||||||
|
(notificationAddress: string, index: number) => {
|
||||||
|
return notificationAddressForm(index, notificationAddress);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addBlankNotificationAddress = () => {
|
||||||
|
const notificationAddresses: string[] =
|
||||||
|
processModel.exception_notification_addresses || [];
|
||||||
|
notificationAddresses.push('');
|
||||||
|
updateProcessModel({
|
||||||
|
exception_notification_addresses: notificationAddresses,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onDisplayNameChanged = (newDisplayName: any) => {
|
const onDisplayNameChanged = (newDisplayName: any) => {
|
||||||
setDisplayNameInvalid(false);
|
setDisplayNameInvalid(false);
|
||||||
const updateDict = { display_name: newDisplayName };
|
const updateDict = { display_name: newDisplayName };
|
||||||
|
@ -182,6 +250,11 @@ export default function ProcessModelForm({
|
||||||
updateProcessModel(updateDict);
|
updateProcessModel(updateDict);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onNotificationTypeChanged = (newNotificationType: string) => {
|
||||||
|
const updateDict = { fault_or_suspend_on_exception: newNotificationType };
|
||||||
|
updateProcessModel(updateDict);
|
||||||
|
};
|
||||||
|
|
||||||
const formElements = () => {
|
const formElements = () => {
|
||||||
const textInputs = [
|
const textInputs = [
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -230,6 +303,49 @@ export default function ProcessModelForm({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
textInputs.push(
|
||||||
|
<Select
|
||||||
|
id="notification-type"
|
||||||
|
defaultValue="fault"
|
||||||
|
labelText="Notification Type"
|
||||||
|
onChange={(event: any) => {
|
||||||
|
onNotificationTypeChanged(event.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectItem value="fault" text="Fault" />
|
||||||
|
<SelectItem value="suspend" text="Suspend" />
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
textInputs.push(<h2>Notification Addresses</h2>);
|
||||||
|
textInputs.push(
|
||||||
|
<Grid>
|
||||||
|
<Column md={8} lg={16} sm={4}>
|
||||||
|
<p className="data-table-description">
|
||||||
|
You can provide one or more addresses to notify if this model fails.
|
||||||
|
</p>
|
||||||
|
</Column>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
textInputs.push(<>{notificationAddressFormArea()}</>);
|
||||||
|
textInputs.push(
|
||||||
|
<Grid>
|
||||||
|
<Column md={4} lg={8} sm={2}>
|
||||||
|
<Button
|
||||||
|
data-qa="add-notification-address-button"
|
||||||
|
renderIcon={AddAlt}
|
||||||
|
className="button-white-background"
|
||||||
|
kind=""
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
addBlankNotificationAddress();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Notification Address
|
||||||
|
</Button>
|
||||||
|
</Column>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
|
||||||
textInputs.push(<h2>Metadata Extractions</h2>);
|
textInputs.push(<h2>Metadata Extractions</h2>);
|
||||||
textInputs.push(
|
textInputs.push(
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|
|
@ -151,6 +151,8 @@ export interface ProcessModel {
|
||||||
files: ProcessFile[];
|
files: ProcessFile[];
|
||||||
parent_groups?: ProcessGroupLite[];
|
parent_groups?: ProcessGroupLite[];
|
||||||
metadata_extraction_paths?: MetadataExtractionPath[];
|
metadata_extraction_paths?: MetadataExtractionPath[];
|
||||||
|
fault_or_suspend_on_exception?: string;
|
||||||
|
exception_notification_addresses?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProcessGroup {
|
export interface ProcessGroup {
|
||||||
|
|
Loading…
Reference in New Issue