mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-02-03 21:24:04 +00:00
Basic scaffolding for onboarding (#400)
* Wedge between InProgressInstances for default view customization * Rename to OnboardingView, stubbed out api in the backend * Flip between InProgressInstances and MyTasks via the backend * WIP * WIP * Basic human task handling * FE lint * Getting ./bin/pyl to pass * Suppress any exceptions during onboarding request * Script to skip onboarding if already started * Getting ./bin/pyl to pass * Better default location * PR feedback * PR feedback * PR feedback - add new endpoint to basic permissions * Fix basic permissions test * Add integration tests * Getting bin_pyl to pass
This commit is contained in:
parent
148bf386e3
commit
43beb916a3
@ -176,6 +176,20 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/onboarding:
|
||||
get:
|
||||
operationId: spiffworkflow_backend.routes.onboarding_controller.get_onboarding
|
||||
summary: Returns information about the next onboarding to show
|
||||
tags:
|
||||
- Status
|
||||
responses:
|
||||
"200":
|
||||
description: Returns info about the next onboarding to show if it exists.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/active-users/updates/{last_visited_identifier}:
|
||||
parameters:
|
||||
- name: last_visited_identifier
|
||||
|
@ -0,0 +1,27 @@
|
||||
"""APIs for dealing with process groups, process models, and process instances."""
|
||||
from contextlib import suppress
|
||||
|
||||
from flask import make_response
|
||||
from flask.wrappers import Response
|
||||
|
||||
from spiffworkflow_backend.routes.process_instances_controller import _process_instance_start
|
||||
|
||||
|
||||
def get_onboarding() -> Response:
|
||||
result = {}
|
||||
|
||||
with suppress(Exception):
|
||||
process_instance, processor = _process_instance_start("site-administration/onboarding")
|
||||
|
||||
if processor is not None:
|
||||
if process_instance.status == "complete":
|
||||
workflow_data = processor.bpmn_process_instance.data
|
||||
result = workflow_data.get("onboarding", {})
|
||||
elif process_instance.status == "user_input_required" and len(process_instance.active_human_tasks) > 0:
|
||||
result = {
|
||||
"type": "user_input_required",
|
||||
"process_instance_id": process_instance.id,
|
||||
"task_id": process_instance.active_human_tasks[0].task_id,
|
||||
}
|
||||
|
||||
return make_response(result, 200)
|
@ -55,11 +55,9 @@ from spiffworkflow_backend.services.task_service import TaskService
|
||||
# )
|
||||
|
||||
|
||||
def process_instance_create(
|
||||
modified_process_model_identifier: str,
|
||||
) -> flask.wrappers.Response:
|
||||
process_model_identifier = _un_modify_modified_process_model_id(modified_process_model_identifier)
|
||||
|
||||
def _process_instance_create(
|
||||
process_model_identifier: str,
|
||||
) -> ProcessInstanceModel:
|
||||
process_model = _get_process_model(process_model_identifier)
|
||||
if process_model.primary_file_name is None:
|
||||
raise ApiError(
|
||||
@ -74,6 +72,15 @@ def process_instance_create(
|
||||
process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier(
|
||||
process_model_identifier, g.user
|
||||
)
|
||||
return process_instance
|
||||
|
||||
|
||||
def process_instance_create(
|
||||
modified_process_model_identifier: str,
|
||||
) -> flask.wrappers.Response:
|
||||
process_model_identifier = _un_modify_modified_process_model_id(modified_process_model_identifier)
|
||||
|
||||
process_instance = _process_instance_create(process_model_identifier)
|
||||
return Response(
|
||||
json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
|
||||
status=201,
|
||||
@ -81,11 +88,9 @@ def process_instance_create(
|
||||
)
|
||||
|
||||
|
||||
def process_instance_run(
|
||||
modified_process_model_identifier: str,
|
||||
process_instance_id: int,
|
||||
) -> flask.wrappers.Response:
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
def _process_instance_run(
|
||||
process_instance: ProcessInstanceModel,
|
||||
) -> ProcessInstanceProcessor | None:
|
||||
if process_instance.status != "not_started":
|
||||
raise ApiError(
|
||||
error_code="process_instance_not_runnable",
|
||||
@ -121,6 +126,16 @@ def process_instance_run(
|
||||
if not current_app.config["SPIFFWORKFLOW_BACKEND_RUN_BACKGROUND_SCHEDULER"]:
|
||||
MessageService.correlate_all_message_instances()
|
||||
|
||||
return processor
|
||||
|
||||
|
||||
def process_instance_run(
|
||||
modified_process_model_identifier: str,
|
||||
process_instance_id: int,
|
||||
) -> flask.wrappers.Response:
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
processor = _process_instance_run(process_instance)
|
||||
|
||||
# for mypy
|
||||
if processor is not None:
|
||||
process_instance_api = ProcessInstanceService.processor_to_process_instance_api(processor)
|
||||
@ -134,6 +149,14 @@ def process_instance_run(
|
||||
return make_response(jsonify(process_instance), 200)
|
||||
|
||||
|
||||
def _process_instance_start(
|
||||
process_model_identifier: str,
|
||||
) -> tuple[ProcessInstanceModel, ProcessInstanceProcessor | None]:
|
||||
process_instance = _process_instance_create(process_model_identifier)
|
||||
processor = _process_instance_run(process_instance)
|
||||
return process_instance, processor
|
||||
|
||||
|
||||
def process_instance_terminate(
|
||||
process_instance_id: int,
|
||||
modified_process_model_identifier: str,
|
||||
|
@ -0,0 +1,22 @@
|
||||
from typing import Any
|
||||
|
||||
from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext
|
||||
from spiffworkflow_backend.scripts.script import Script
|
||||
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
||||
|
||||
|
||||
class UserHasStartedInstance(Script):
|
||||
@staticmethod
|
||||
def requires_privileged_permissions() -> bool:
|
||||
"""We have deemed this function safe to run without elevated permissions."""
|
||||
return False
|
||||
|
||||
def get_description(self) -> str:
|
||||
return """Returns boolean to indicate if the user has started an instance of the current process model."""
|
||||
|
||||
def run(self, script_attributes_context: ScriptAttributesContext, *_args: Any, **kwargs: Any) -> Any:
|
||||
process_model_identifer = script_attributes_context.process_model_identifier
|
||||
if process_model_identifer is not None:
|
||||
return ProcessInstanceService.user_has_started_instance(process_model_identifer)
|
||||
else:
|
||||
return False
|
@ -510,6 +510,7 @@ class AuthorizationService:
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/service-tasks"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/user-groups/for-current-user"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/users/search"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/onboarding"))
|
||||
|
||||
permissions_to_assign.append(
|
||||
PermissionToAssign(permission="read", target_uri="/process-instances/report-metadata")
|
||||
|
@ -44,6 +44,18 @@ class ProcessInstanceService:
|
||||
FILE_DATA_DIGEST_PREFIX = "spifffiledatadigest+"
|
||||
TASK_STATE_LOCKED = "locked"
|
||||
|
||||
@staticmethod
|
||||
def user_has_started_instance(process_model_identifier: str) -> bool:
|
||||
started_instance = (
|
||||
db.session.query(ProcessInstanceModel)
|
||||
.filter(
|
||||
ProcessInstanceModel.process_model_identifier == process_model_identifier,
|
||||
ProcessInstanceModel.status != "not_started",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
return started_instance is not None
|
||||
|
||||
@staticmethod
|
||||
def next_start_event_configuration(process_instance_model: ProcessInstanceModel) -> StartConfiguration:
|
||||
try:
|
||||
|
43
spiffworkflow-backend/tests/data/onboarding/onboarding.bpmn
Normal file
43
spiffworkflow-backend/tests/data/onboarding/onboarding.bpmn
Normal file
@ -0,0 +1,43 @@
|
||||
<?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: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="Process_109on5e" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_0025g6c</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:endEvent id="Event_1w4e3lr">
|
||||
<bpmn:incoming>Flow_01ol0nb</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_01ol0nb" sourceRef="Activity_0907t1z" targetRef="Event_1w4e3lr" />
|
||||
<bpmn:scriptTask id="Activity_0907t1z" name="change default view">
|
||||
<bpmn:incoming>Flow_0025g6c</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_01ol0nb</bpmn:outgoing>
|
||||
<bpmn:script>onboarding = {
|
||||
"type": "default_view",
|
||||
"value": "my_tasks",
|
||||
}</bpmn:script>
|
||||
</bpmn:scriptTask>
|
||||
<bpmn:sequenceFlow id="Flow_0025g6c" sourceRef="StartEvent_1" targetRef="Activity_0907t1z" />
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_109on5e">
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="112" y="-88" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_1w4e3lr_di" bpmnElement="Event_1w4e3lr">
|
||||
<dc:Bounds x="482" y="-88" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0lidiyr_di" bpmnElement="Activity_0907t1z">
|
||||
<dc:Bounds x="260" y="-110" width="100" height="80" />
|
||||
<bpmndi:BPMNLabel />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_0025g6c_di" bpmnElement="Flow_0025g6c">
|
||||
<di:waypoint x="148" y="-70" />
|
||||
<di:waypoint x="260" y="-70" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_01ol0nb_di" bpmnElement="Flow_01ol0nb">
|
||||
<di:waypoint x="360" y="-70" />
|
||||
<di:waypoint x="482" y="-70" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
@ -0,0 +1,48 @@
|
||||
from flask import Flask
|
||||
from flask.testing import FlaskClient
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
|
||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
|
||||
|
||||
class TestOnboarding(BaseTest):
|
||||
def test_returns_nothing_if_no_onboarding_model(
|
||||
self,
|
||||
app: Flask,
|
||||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
results = client.get(
|
||||
"/v1.0/onboarding",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
|
||||
assert results.status_code == 200
|
||||
assert results.json == {}
|
||||
|
||||
def test_returns_onboarding_if_onboarding_model(
|
||||
self,
|
||||
app: Flask,
|
||||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
process_group_id = "site-administration"
|
||||
process_model_id = "onboarding"
|
||||
bpmn_file_location = "onboarding"
|
||||
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_location=bpmn_file_location,
|
||||
)
|
||||
|
||||
results = client.get(
|
||||
"/v1.0/onboarding",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
|
||||
assert results.status_code == 200
|
||||
assert results.json == {"type": "default_view", "value": "my_tasks"}
|
@ -280,6 +280,7 @@ class TestAuthorizationService(BaseTest):
|
||||
("/active-users/*", "create"),
|
||||
("/connector-proxy/typeahead/*", "read"),
|
||||
("/debug/version-info", "read"),
|
||||
("/onboarding", "read"),
|
||||
("/process-groups", "read"),
|
||||
("/process-instances/find-by-id/*", "read"),
|
||||
("/process-instances/for-me", "create"),
|
||||
|
@ -10,6 +10,13 @@ export interface Secret {
|
||||
creator_user_id: string;
|
||||
}
|
||||
|
||||
export interface Onboarding {
|
||||
type?: string;
|
||||
value?: string;
|
||||
process_instance_id?: string;
|
||||
task_id?: string;
|
||||
}
|
||||
|
||||
export interface ProcessData {
|
||||
process_data_identifier: string;
|
||||
process_data_value: any;
|
||||
|
@ -6,6 +6,7 @@ import MyTasks from './MyTasks';
|
||||
import CompletedInstances from './CompletedInstances';
|
||||
import CreateNewInstance from './CreateNewInstance';
|
||||
import InProgressInstances from './InProgressInstances';
|
||||
import OnboardingView from './OnboardingView';
|
||||
|
||||
export default function HomePageRoutes() {
|
||||
const location = useLocation();
|
||||
@ -50,7 +51,7 @@ export default function HomePageRoutes() {
|
||||
<div className="fixed-width-container">
|
||||
{renderTabs()}
|
||||
<Routes>
|
||||
<Route path="/" element={<InProgressInstances />} />
|
||||
<Route path="/" element={<OnboardingView />} />
|
||||
<Route path="my-tasks" element={<MyTasks />} />
|
||||
<Route path=":process_instance_id/:task_id" element={<TaskShow />} />
|
||||
<Route path="grouped" element={<InProgressInstances />} />
|
||||
|
41
spiffworkflow-frontend/src/routes/OnboardingView.tsx
Normal file
41
spiffworkflow-frontend/src/routes/OnboardingView.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import HttpService from '../services/HttpService';
|
||||
import InProgressInstances from './InProgressInstances';
|
||||
import { Onboarding } from '../interfaces';
|
||||
import MyTasks from './MyTasks';
|
||||
|
||||
export default function OnboardingView() {
|
||||
const [onboarding, setOnboarding] = useState<Onboarding | null>(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/onboarding`,
|
||||
successCallback: setOnboarding,
|
||||
});
|
||||
}, [setOnboarding]);
|
||||
|
||||
const onboardingElement = () => {
|
||||
if (onboarding) {
|
||||
if (onboarding.type === 'default_view') {
|
||||
if (onboarding.value === 'my_tasks') {
|
||||
return <MyTasks />;
|
||||
}
|
||||
} else if (
|
||||
onboarding.type === 'user_input_required' &&
|
||||
onboarding.process_instance_id &&
|
||||
onboarding.task_id
|
||||
) {
|
||||
navigate(
|
||||
`/tasks/${onboarding.process_instance_id}/${onboarding.task_id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <InProgressInstances />;
|
||||
};
|
||||
|
||||
return onboardingElement();
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user