Merge branch 'main' of github.com:sartography/spiff-arena

This commit is contained in:
jasquat 2023-05-03 10:17:35 -04:00
commit 742a73e0cd
No known key found for this signature in database
17 changed files with 914 additions and 378 deletions

View File

@ -18,13 +18,13 @@ def setup_database_uri(app: Flask) -> None:
if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI") is None:
database_name = f"spiffworkflow_backend_{app.config['ENV_IDENTIFIER']}"
if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "sqlite":
app.config["SQLALCHEMY_DATABASE_URI"] = (
f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3"
)
app.config[
"SQLALCHEMY_DATABASE_URI"
] = f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3"
elif app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "postgres":
app.config["SQLALCHEMY_DATABASE_URI"] = (
f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}"
)
app.config[
"SQLALCHEMY_DATABASE_URI"
] = f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}"
else:
# use pswd to trick flake8 with hardcoded passwords
db_pswd = app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD")

View File

@ -129,9 +129,9 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
def serialized_with_metadata(self) -> dict[str, Any]:
process_instance_attributes = self.serialized
process_instance_attributes["process_metadata"] = self.process_metadata
process_instance_attributes["process_model_with_diagram_identifier"] = (
self.process_model_with_diagram_identifier
)
process_instance_attributes[
"process_model_with_diagram_identifier"
] = self.process_model_with_diagram_identifier
return process_instance_attributes
@property

View File

@ -2,6 +2,7 @@
import enum
from dataclasses import dataclass
from typing import Any
from typing import List
from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
@ -85,6 +86,7 @@ class TaskModel(SpiffworkflowBaseDBModel):
can_complete: Optional[bool] = None
extensions: Optional[dict] = None
name_for_display: Optional[str] = None
signal_buttons: Optional[List[dict]] = None
def get_data(self) -> dict:
return {**self.python_env_data(), **self.json_data()}

View File

@ -18,7 +18,6 @@ from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
)
from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema
from spiffworkflow_backend.models.process_instance_file_data import (
ProcessInstanceFileDataModel,
)
@ -32,6 +31,7 @@ from spiffworkflow_backend.services.process_caller_service import ProcessCallerS
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
from spiffworkflow_backend.services.process_model_service import ProcessModelService
@ -199,16 +199,13 @@ def send_bpmn_event(
if process_instance:
processor = ProcessInstanceProcessor(process_instance)
processor.send_bpmn_event(body)
task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
return make_response(jsonify(task), 200)
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",
)
def _commit_and_push_to_git(message: str) -> None:

View File

@ -289,6 +289,7 @@ def task_show(process_instance_id: int, task_guid: str = "next") -> flask.wrappe
task_model = _get_task_model_from_guid_or_raise(task_guid, process_instance_id)
task_definition = task_model.task_definition
extensions = TaskService.get_extensions_from_task_model(task_model)
task_model.signal_buttons = TaskService.get_ready_signals_with_button_labels(process_instance_id)
if "properties" in extensions:
properties = extensions["properties"]

View File

@ -1,28 +0,0 @@
"""Get_process_info."""
from typing import Any
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
)
from spiffworkflow_backend.scripts.script import Script
# DEPRECATED: please use GetToplevelProcessInfo instead
class GetProcessInfo(Script):
"""GetProcessInfo."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str:
"""Get_description."""
return """Returns a dictionary of information about the currently running process."""
def run(self, script_attributes_context: ScriptAttributesContext, *_args: Any, **kwargs: Any) -> Any:
"""Run."""
return {
"process_instance_id": script_attributes_context.process_instance_id,
"process_model_identifier": script_attributes_context.process_model_identifier,
}

View File

@ -423,9 +423,9 @@ class ProcessInstanceProcessor:
tld.process_instance_id = process_instance_model.id
# we want this to be the fully qualified path to the process model including all group subcomponents
current_app.config["THREAD_LOCAL_DATA"].process_model_identifier = (
f"{process_instance_model.process_model_identifier}"
)
current_app.config[
"THREAD_LOCAL_DATA"
].process_model_identifier = f"{process_instance_model.process_model_identifier}"
self.process_instance_model = process_instance_model
self.process_model_service = ProcessModelService()
@ -585,9 +585,9 @@ class ProcessInstanceProcessor:
bpmn_subprocess_definition.bpmn_identifier
] = bpmn_process_definition_dict
spiff_bpmn_process_dict["subprocess_specs"][bpmn_subprocess_definition.bpmn_identifier]["task_specs"] = {}
bpmn_subprocess_definition_bpmn_identifiers[bpmn_subprocess_definition.id] = (
bpmn_subprocess_definition.bpmn_identifier
)
bpmn_subprocess_definition_bpmn_identifiers[
bpmn_subprocess_definition.id
] = bpmn_subprocess_definition.bpmn_identifier
task_definitions = TaskDefinitionModel.query.filter(
TaskDefinitionModel.bpmn_process_definition_id.in_( # type: ignore

View File

@ -2,6 +2,7 @@ import copy
import json
import time
from hashlib import sha256
from typing import List
from typing import Optional
from typing import Tuple
from typing import TypedDict
@ -607,6 +608,28 @@ class TaskService:
)
return extensions
@classmethod
def get_ready_signals_with_button_labels(cls, process_instance_id: int) -> list[dict]:
waiting_tasks: List[TaskModel] = TaskModel.query.filter_by(
state="WAITING", process_instance_id=process_instance_id
).all()
result = []
for task_model in waiting_tasks:
task_definition = task_model.task_definition
extensions: dict = (
task_definition.properties_json["extensions"]
if "extensions" in task_definition.properties_json
else {}
)
event_definition: dict = (
task_definition.properties_json["event_definition"]
if "event_definition" in task_definition.properties_json
else {}
)
if "signalButtonLabel" in extensions and "name" in event_definition:
result.append({"event": event_definition, "label": extensions["signalButtonLabel"]})
return result
@classmethod
def get_spec_reference_from_bpmn_process(cls, bpmn_process: BpmnProcessModel) -> SpecReferenceCache:
"""Get the bpmn file for a given task model.

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="SpiffCatchEventExtensions" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0elszck</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0elszck" sourceRef="StartEvent_1" targetRef="Activity_0cmmlen" />
<bpmn:endEvent id="Event_1mjvim4">
<bpmn:incoming>Flow_1akz8b3</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1akz8b3" sourceRef="Activity_0cmmlen" targetRef="Event_1mjvim4" />
<bpmn:sequenceFlow id="Flow_0uenxs3" sourceRef="SpamEvent" targetRef="Activity_1u4om4i" />
<bpmn:endEvent id="Event_1dvll15">
<bpmn:incoming>Flow_16bzuvz</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_16bzuvz" sourceRef="Activity_1u4om4i" targetRef="Event_1dvll15" />
<bpmn:manualTask id="Activity_0cmmlen" name="My Manual Task">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser># Welcome
This manual task has Two Buttons! The first is standard submit button that will take you to the end. The second button will fire a signal event and take you to a different manual task.</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0elszck</bpmn:incoming>
<bpmn:outgoing>Flow_1akz8b3</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:manualTask id="Activity_1u4om4i" name="Spam Message">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser># Spam Eaten!
Congratulations! You have selected the Eat Additional Spam option, which opens up new doors to vast previously uncharted culinary eating experiences! Oh the Joy! Oh the Reward! Sweet savory wonderful Spam! </spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0uenxs3</bpmn:incoming>
<bpmn:outgoing>Flow_16bzuvz</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:boundaryEvent id="SpamEvent" name="Spam Event" attachedToRef="Activity_0cmmlen">
<bpmn:extensionElements>
<spiffworkflow:signalButtonLabel>Eat Spam</spiffworkflow:signalButtonLabel>
</bpmn:extensionElements>
<bpmn:outgoing>Flow_0uenxs3</bpmn:outgoing>
<bpmn:signalEventDefinition id="SignalEventDefinition_11tlwya" signalRef="Signal_17t90lm" />
</bpmn:boundaryEvent>
</bpmn:process>
<bpmn:signal id="Signal_17t90lm" name="eat_spam" />
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_z1jgvu5">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1mjvim4_di" bpmnElement="Event_1mjvim4">
<dc:Bounds x="432" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1dvll15_di" bpmnElement="Event_1dvll15">
<dc:Bounds x="562" y="282" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0zxmtux_di" bpmnElement="Activity_0cmmlen">
<dc:Bounds x="270" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0tll58x_di" bpmnElement="Activity_1u4om4i">
<dc:Bounds x="410" y="260" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0vnraxp_di" bpmnElement="SpamEvent">
<dc:Bounds x="322" y="199" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="311" y="242" width="61" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0elszck_di" bpmnElement="Flow_0elszck">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1akz8b3_di" bpmnElement="Flow_1akz8b3">
<di:waypoint x="370" y="177" />
<di:waypoint x="432" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0uenxs3_di" bpmnElement="Flow_0uenxs3">
<di:waypoint x="340" y="235" />
<di:waypoint x="340" y="300" />
<di:waypoint x="410" y="300" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_16bzuvz_di" bpmnElement="Flow_16bzuvz">
<di:waypoint x="510" y="300" />
<di:waypoint x="562" y="300" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -2662,7 +2662,8 @@ class TestProcessApi(BaseTest):
)
assert response.status_code == 200
assert response.json is not None
assert response.json["status"] == "complete"
assert response.json["type"] == "End Event"
assert response.json["state"] == "COMPLETED"
response = client.get(
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/task-info?all_tasks=true",

View File

@ -156,3 +156,30 @@ class TestTaskService(BaseTest):
assert task_model_level_3 is not None
bpmn_process = TaskService.bpmn_process_for_called_activity_or_top_level_process(task_model_level_3)
assert bpmn_process.bpmn_process_definition.bpmn_identifier == "Level3"
def test_get_button_labels_for_waiting_signal_event_tasks(
self,
app: Flask,
with_db_and_bpmn_file_cleanup: None,
) -> None:
process_model = load_test_spec(
"test_group/signal_event_extensions",
process_model_source_directory="signal_event_extensions",
bpmn_file_name="signal_event_extensions",
)
load_test_spec(
"test_group/SpiffCatchEventExtensions",
process_model_source_directory="call_activity_nested",
bpmn_file_name="SpiffCatchEventExtensions",
)
process_instance = self.create_process_instance_from_process_model(process_model)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True, execution_strategy_name="greedy")
events = TaskService.get_ready_signals_with_button_labels(process_instance.id)
assert len(events) == 1
signal_event = events[0]
assert signal_event["event"]["name"] == "eat_spam"
assert signal_event["event"]["typename"] == "SignalEventDefinition"
assert signal_event["label"] == "Eat Spam"
print(events)

File diff suppressed because it is too large Load Diff

View File

@ -31,14 +31,14 @@
"autoprefixer": "10.4.8",
"axios": "^0.27.2",
"bootstrap": "^5.2.0",
"bpmn-js": "^9.3.2",
"bpmn-js-properties-panel": "^1.10.0",
"bpmn-js": "^13.0.0",
"bpmn-js-properties-panel": "^1.22.0",
"bpmn-js-spiffworkflow": "github:sartography/bpmn-js-spiffworkflow#main",
"cookie": "^0.5.0",
"craco": "^0.0.3",
"cypress-slow-down": "^1.2.1",
"date-fns": "^2.28.0",
"diagram-js": "^8.5.0",
"diagram-js": "^11.9.1",
"dmn-js": "^12.2.0",
"dmn-js-properties-panel": "^1.1",
"dmn-js-shared": "^12.1.1",

View File

@ -1,7 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'bpmn... Remove this comment to see the full error message
import BpmnModeler from 'bpmn-js/lib/Modeler';
// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'bpmn... Remove this comment to see the full error message
import BpmnViewer from 'bpmn-js/lib/Viewer';
import {
BpmnPropertiesPanelModule,
@ -144,11 +142,14 @@ export default function ReactDiagramEditor({
}
const temp = document.createElement('template');
const panelId: string =
diagramType === 'readonly'
? 'hidden-properties-panel'
: 'js-properties-panel';
temp.innerHTML = `
<div class="content with-diagram" id="js-drop-zone">
<div class="canvas ${canvasClass}" id="canvas"
></div>
<div class="properties-panel-parent" id="js-properties-panel"></div>
<div class="canvas ${canvasClass}" id="canvas"></div>
<div class="properties-panel-parent" id="${panelId}"></div>
</div>
`;
const frag = temp.content;

View File

@ -37,6 +37,11 @@ export interface EventDefinition {
message_var?: string;
}
export interface SignalButton {
label: string;
event: EventDefinition;
}
// TODO: merge with ProcessInstanceTask
export interface Task {
id: number;
@ -60,6 +65,7 @@ export interface Task {
can_complete: boolean;
form_schema: any;
form_ui_schema: any;
signal_buttons: SignalButton[];
}
export interface ProcessInstanceTask {

View File

@ -58,7 +58,6 @@ export default function ProcessInterstitial() {
// Added this seperate use effect so that the timer interval will be cleared if
// we end up redirecting back to the TaskShow page.
if (shouldRedirect(lastTask)) {
setState('REDIRECTING');
lastTask.properties.instructionsForEndUser = '';
const timerId = setInterval(() => {
navigate(`/tasks/${lastTask.process_instance_id}/${lastTask.id}`);
@ -103,9 +102,9 @@ export default function ProcessInterstitial() {
const getReturnHomeButton = (index: number) => {
if (
index === 0 &&
state !== 'REDIRECTING' &&
!shouldRedirect(lastTask) &&
['WAITING', 'ERROR', 'LOCKED', 'COMPLETED', 'READY'].includes(getStatus())
)
) {
return (
<div style={{ padding: '10px 0 0 0' }}>
<Button kind="secondary" onClick={() => navigate(`/tasks`)}>
@ -113,6 +112,7 @@ export default function ProcessInterstitial() {
</Button>
</div>
);
}
return '';
};
@ -165,7 +165,7 @@ export default function ProcessInterstitial() {
/** In the event there is no task information and the connection closed,
* redirect to the home page. */
if (state === 'closed' && lastTask === null) {
if (state === 'CLOSED' && lastTask === null) {
navigate(`/tasks`);
}
if (lastTask) {

View File

@ -18,7 +18,7 @@ import Form from '../themes/carbon';
import HttpService from '../services/HttpService';
import useAPIError from '../hooks/UseApiError';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import { Task } from '../interfaces';
import { EventDefinition, Task } from '../interfaces';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import InstructionsForEndUser from '../components/InstructionsForEndUser';
@ -195,6 +195,23 @@ export default function TaskShow() {
});
};
const handleSignalSubmit = (event: EventDefinition) => {
if (disabled || !task) {
return;
}
HttpService.makeCallToBackend({
path: `/send-event/${modifyProcessIdentifierForPathParam(
task.process_model_identifier
)}/${params.process_instance_id}`,
successCallback: processSubmitResult,
failureCallback: (error: any) => {
addError(error);
},
httpMethod: 'POST',
postBody: event,
});
};
const buildTaskNavigation = () => {
let userTasksElement;
let selectedTabIndex = 0;
@ -355,6 +372,17 @@ export default function TaskShow() {
{submitButtonText}
</Button>
{saveAsDraftButton}
<>
{task.signal_buttons.map((signal) => (
<Button
name="signal.signal"
disabled={disabled}
onClick={() => handleSignalSubmit(signal.event)}
>
{signal.label}
</Button>
))}
</>
</ButtonSet>
);
}