Merge pull request #95 from sartography/feature/find_by_process_instance_id
Feature/find by process instance
This commit is contained in:
commit
6dff495aa6
|
@ -946,6 +946,27 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/Workflow"
|
||||
|
||||
/process-instances/find-by-id/{process_instance_id}:
|
||||
parameters:
|
||||
- name: process_instance_id
|
||||
in: path
|
||||
required: true
|
||||
description: The unique id of an existing process instance.
|
||||
schema:
|
||||
type: integer
|
||||
get:
|
||||
operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_find_by_id
|
||||
summary: Find a process instance based on its id only
|
||||
tags:
|
||||
- Process Instances
|
||||
responses:
|
||||
"200":
|
||||
description: One Process Instance
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Workflow"
|
||||
|
||||
/process-instances/{modified_process_model_identifier}/{process_instance_id}:
|
||||
parameters:
|
||||
- name: modified_process_model_identifier
|
||||
|
|
|
@ -30,6 +30,12 @@ permissions:
|
|||
allowed_permissions: [read]
|
||||
uri: /*
|
||||
|
||||
process-instances-find-by-id:
|
||||
groups: [everybody]
|
||||
users: []
|
||||
allowed_permissions: [read]
|
||||
uri: /process-instances/find-by-id/*
|
||||
|
||||
tasks-crud:
|
||||
groups: [everybody]
|
||||
users: []
|
||||
|
|
|
@ -58,6 +58,14 @@ class ProcessModelInfo:
|
|||
"""Id_for_file_path."""
|
||||
return self.id.replace("/", os.sep)
|
||||
|
||||
@classmethod
|
||||
def modify_process_identifier_for_path_param(cls, identifier: str) -> str:
|
||||
"""Identifier."""
|
||||
if "\\" in identifier:
|
||||
raise Exception(f"Found backslash in identifier: {identifier}")
|
||||
|
||||
return identifier.replace("/", ":")
|
||||
|
||||
|
||||
class ProcessModelInfoSchema(Schema):
|
||||
"""ProcessModelInfoSchema."""
|
||||
|
|
|
@ -31,6 +31,7 @@ from spiffworkflow_backend.models.process_instance_metadata import (
|
|||
from spiffworkflow_backend.models.process_instance_report import (
|
||||
ProcessInstanceReportModel,
|
||||
)
|
||||
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
||||
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
|
||||
from spiffworkflow_backend.models.spec_reference import SpecReferenceNotFoundError
|
||||
from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel
|
||||
|
@ -43,6 +44,7 @@ from spiffworkflow_backend.routes.process_api_blueprint import _get_process_mode
|
|||
from spiffworkflow_backend.routes.process_api_blueprint import (
|
||||
_un_modify_modified_process_model_id,
|
||||
)
|
||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||
from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService
|
||||
from spiffworkflow_backend.services.git_service import GitCommandError
|
||||
from spiffworkflow_backend.services.git_service import GitService
|
||||
|
@ -88,9 +90,7 @@ def process_instance_run(
|
|||
do_engine_steps: bool = True,
|
||||
) -> flask.wrappers.Response:
|
||||
"""Process_instance_run."""
|
||||
process_instance = ProcessInstanceService().get_process_instance(
|
||||
process_instance_id
|
||||
)
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
if process_instance.status != "not_started":
|
||||
raise ApiError(
|
||||
error_code="process_instance_not_runnable",
|
||||
|
@ -138,9 +138,7 @@ def process_instance_terminate(
|
|||
modified_process_model_identifier: str,
|
||||
) -> flask.wrappers.Response:
|
||||
"""Process_instance_run."""
|
||||
process_instance = ProcessInstanceService().get_process_instance(
|
||||
process_instance_id
|
||||
)
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.terminate()
|
||||
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
|
||||
|
@ -151,9 +149,7 @@ def process_instance_suspend(
|
|||
modified_process_model_identifier: str,
|
||||
) -> flask.wrappers.Response:
|
||||
"""Process_instance_suspend."""
|
||||
process_instance = ProcessInstanceService().get_process_instance(
|
||||
process_instance_id
|
||||
)
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.suspend()
|
||||
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
|
||||
|
@ -164,9 +160,7 @@ def process_instance_resume(
|
|||
modified_process_model_identifier: str,
|
||||
) -> flask.wrappers.Response:
|
||||
"""Process_instance_resume."""
|
||||
process_instance = ProcessInstanceService().get_process_instance(
|
||||
process_instance_id
|
||||
)
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.resume()
|
||||
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
|
||||
|
@ -575,14 +569,43 @@ def process_instance_reset(
|
|||
spiff_step: int = 0,
|
||||
) -> flask.wrappers.Response:
|
||||
"""Reset a process instance to a particular step."""
|
||||
process_instance = ProcessInstanceService().get_process_instance(
|
||||
process_instance_id
|
||||
)
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.reset_process(spiff_step)
|
||||
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
|
||||
|
||||
|
||||
def process_instance_find_by_id(
|
||||
process_instance_id: int,
|
||||
) -> flask.wrappers.Response:
|
||||
"""Process_instance_find_by_id."""
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
modified_process_model_identifier = (
|
||||
ProcessModelInfo.modify_process_identifier_for_path_param(
|
||||
process_instance.process_model_identifier
|
||||
)
|
||||
)
|
||||
process_instance_uri = (
|
||||
f"/process-instances/{modified_process_model_identifier}/{process_instance.id}"
|
||||
)
|
||||
has_permission = AuthorizationService.user_has_permission(
|
||||
user=g.user,
|
||||
permission="read",
|
||||
target_uri=process_instance_uri,
|
||||
)
|
||||
|
||||
uri_type = None
|
||||
if not has_permission:
|
||||
process_instance = _find_process_instance_for_me_or_raise(process_instance_id)
|
||||
uri_type = "for-me"
|
||||
|
||||
response_json = {
|
||||
"process_instance": process_instance,
|
||||
"uri_type": uri_type,
|
||||
}
|
||||
return make_response(jsonify(response_json), 200)
|
||||
|
||||
|
||||
def _get_process_instance(
|
||||
modified_process_model_identifier: str,
|
||||
process_instance: ProcessInstanceModel,
|
||||
|
|
|
@ -624,6 +624,84 @@ class AuthorizationService:
|
|||
|
||||
return permissions_to_assign
|
||||
|
||||
@classmethod
|
||||
def set_basic_permissions(cls) -> list[PermissionToAssign]:
|
||||
"""Set_basic_permissions."""
|
||||
permissions_to_assign: list[PermissionToAssign] = []
|
||||
permissions_to_assign.append(
|
||||
PermissionToAssign(
|
||||
permission="read", target_uri="/process-instances/for-me"
|
||||
)
|
||||
)
|
||||
permissions_to_assign.append(
|
||||
PermissionToAssign(permission="read", target_uri="/processes")
|
||||
)
|
||||
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="/process-instances/find-by-id/*"
|
||||
)
|
||||
)
|
||||
|
||||
for permission in ["create", "read", "update", "delete"]:
|
||||
permissions_to_assign.append(
|
||||
PermissionToAssign(
|
||||
permission=permission, target_uri="/process-instances/reports/*"
|
||||
)
|
||||
)
|
||||
permissions_to_assign.append(
|
||||
PermissionToAssign(permission=permission, target_uri="/tasks/*")
|
||||
)
|
||||
return permissions_to_assign
|
||||
|
||||
@classmethod
|
||||
def set_process_group_permissions(
|
||||
cls, target: str, permission_set: str
|
||||
) -> list[PermissionToAssign]:
|
||||
"""Set_process_group_permissions."""
|
||||
permissions_to_assign: list[PermissionToAssign] = []
|
||||
process_group_identifier = (
|
||||
target.removeprefix("PG:").replace("/", ":").removeprefix(":")
|
||||
)
|
||||
process_related_path_segment = f"{process_group_identifier}:*"
|
||||
if process_group_identifier == "ALL":
|
||||
process_related_path_segment = "*"
|
||||
target_uris = [
|
||||
f"/process-groups/{process_related_path_segment}",
|
||||
f"/process-models/{process_related_path_segment}",
|
||||
]
|
||||
permissions_to_assign = permissions_to_assign + cls.get_permissions_to_assign(
|
||||
permission_set, process_related_path_segment, target_uris
|
||||
)
|
||||
return permissions_to_assign
|
||||
|
||||
@classmethod
|
||||
def set_process_model_permissions(
|
||||
cls, target: str, permission_set: str
|
||||
) -> list[PermissionToAssign]:
|
||||
"""Set_process_model_permissions."""
|
||||
permissions_to_assign: list[PermissionToAssign] = []
|
||||
process_model_identifier = (
|
||||
target.removeprefix("PM:").replace("/", ":").removeprefix(":")
|
||||
)
|
||||
process_related_path_segment = f"{process_model_identifier}/*"
|
||||
|
||||
if process_model_identifier == "ALL":
|
||||
process_related_path_segment = "*"
|
||||
|
||||
target_uris = [f"/process-models/{process_related_path_segment}"]
|
||||
permissions_to_assign = permissions_to_assign + cls.get_permissions_to_assign(
|
||||
permission_set, process_related_path_segment, target_uris
|
||||
)
|
||||
return permissions_to_assign
|
||||
|
||||
@classmethod
|
||||
def explode_permissions(
|
||||
cls, permission_set: str, target: str
|
||||
|
@ -654,72 +732,20 @@ class AuthorizationService:
|
|||
permissions = ["create", "read", "update", "delete"]
|
||||
|
||||
if target.startswith("PG:"):
|
||||
process_group_identifier = (
|
||||
target.removeprefix("PG:").replace("/", ":").removeprefix(":")
|
||||
permissions_to_assign += cls.set_process_group_permissions(
|
||||
target, permission_set
|
||||
)
|
||||
process_related_path_segment = f"{process_group_identifier}:*"
|
||||
if process_group_identifier == "ALL":
|
||||
process_related_path_segment = "*"
|
||||
target_uris = [
|
||||
f"/process-groups/{process_related_path_segment}",
|
||||
f"/process-models/{process_related_path_segment}",
|
||||
]
|
||||
permissions_to_assign = (
|
||||
permissions_to_assign
|
||||
+ cls.get_permissions_to_assign(
|
||||
permission_set, process_related_path_segment, target_uris
|
||||
)
|
||||
)
|
||||
|
||||
elif target.startswith("PM:"):
|
||||
process_model_identifier = (
|
||||
target.removeprefix("PM:").replace("/", ":").removeprefix(":")
|
||||
permissions_to_assign += cls.set_process_model_permissions(
|
||||
target, permission_set
|
||||
)
|
||||
process_related_path_segment = f"{process_model_identifier}/*"
|
||||
|
||||
if process_model_identifier == "ALL":
|
||||
process_related_path_segment = "*"
|
||||
|
||||
target_uris = [f"/process-models/{process_related_path_segment}"]
|
||||
permissions_to_assign = (
|
||||
permissions_to_assign
|
||||
+ cls.get_permissions_to_assign(
|
||||
permission_set, process_related_path_segment, target_uris
|
||||
)
|
||||
)
|
||||
|
||||
elif permission_set == "start":
|
||||
raise InvalidPermissionError(
|
||||
"Permission 'start' is only available for macros PM and PG."
|
||||
)
|
||||
|
||||
elif target.startswith("BASIC"):
|
||||
permissions_to_assign.append(
|
||||
PermissionToAssign(
|
||||
permission="read", target_uri="/process-instances/for-me"
|
||||
)
|
||||
)
|
||||
permissions_to_assign.append(
|
||||
PermissionToAssign(permission="read", target_uri="/processes")
|
||||
)
|
||||
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"
|
||||
)
|
||||
)
|
||||
|
||||
for permission in ["create", "read", "update", "delete"]:
|
||||
permissions_to_assign.append(
|
||||
PermissionToAssign(
|
||||
permission=permission, target_uri="/process-instances/reports/*"
|
||||
)
|
||||
)
|
||||
permissions_to_assign.append(
|
||||
PermissionToAssign(permission=permission, target_uri="/tasks/*")
|
||||
)
|
||||
permissions_to_assign += cls.set_basic_permissions()
|
||||
elif target == "ALL":
|
||||
for permission in permissions:
|
||||
permissions_to_assign.append(
|
||||
|
|
|
@ -354,11 +354,8 @@ class BaseTest:
|
|||
assert has_permission is expected_result
|
||||
|
||||
def modify_process_identifier_for_path_param(self, identifier: str) -> str:
|
||||
"""Identifier."""
|
||||
if "\\" in identifier:
|
||||
raise Exception(f"Found backslash in identifier: {identifier}")
|
||||
|
||||
return identifier.replace("/", ":")
|
||||
"""Modify_process_identifier_for_path_param."""
|
||||
return ProcessModelInfo.modify_process_identifier_for_path_param(identifier)
|
||||
|
||||
def un_modify_modified_process_identifier_for_path_param(
|
||||
self, modified_identifier: str
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
"""Test_users_controller."""
|
||||
from flask.app import Flask
|
||||
from flask.testing import FlaskClient
|
||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
|
||||
|
||||
class TestProcessInstancesController(BaseTest):
|
||||
"""TestProcessInstancesController."""
|
||||
|
||||
def test_find_by_id(
|
||||
self,
|
||||
app: Flask,
|
||||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
"""Test_user_search_returns_a_user."""
|
||||
user_one = self.create_user_with_permission(
|
||||
username="user_one", target_uri="/process-instances/find-by-id/*"
|
||||
)
|
||||
user_two = self.create_user_with_permission(
|
||||
username="user_two", target_uri="/process-instances/find-by-id/*"
|
||||
)
|
||||
|
||||
process_model = load_test_spec(
|
||||
process_model_id="group/sample",
|
||||
bpmn_file_name="sample.bpmn",
|
||||
process_model_source_directory="sample",
|
||||
)
|
||||
process_instance = self.create_process_instance_from_process_model(
|
||||
process_model=process_model, user=user_one
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
f"/v1.0/process-instances/find-by-id/{process_instance.id}",
|
||||
headers=self.logged_in_headers(user_one),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json
|
||||
assert 'process_instance' in response.json
|
||||
assert response.json['process_instance']["id"] == process_instance.id
|
||||
assert response.json['uri_type'] == 'for-me'
|
||||
|
||||
response = client.get(
|
||||
f"/v1.0/process-instances/find-by-id/{process_instance.id}",
|
||||
headers=self.logged_in_headers(user_two),
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
response = client.get(
|
||||
f"/v1.0/process-instances/find-by-id/{process_instance.id}",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json
|
||||
assert 'process_instance' in response.json
|
||||
assert response.json['process_instance']["id"] == process_instance.id
|
||||
assert response.json['uri_type'] is None
|
|
@ -277,6 +277,7 @@ class TestAuthorizationService(BaseTest):
|
|||
) -> None:
|
||||
"""Test_explode_permissions_basic."""
|
||||
expected_permissions = [
|
||||
("/process-instances/find-by-id/*", "read"),
|
||||
("/process-instances/for-me", "read"),
|
||||
("/process-instances/reports/*", "create"),
|
||||
("/process-instances/reports/*", "delete"),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
convertSecondsToFormattedDateString,
|
||||
isInteger,
|
||||
slugifyString,
|
||||
underscorizeString,
|
||||
} from './helpers';
|
||||
|
@ -20,3 +21,11 @@ test('it can keep the correct date when converting seconds to date', () => {
|
|||
const dateString = convertSecondsToFormattedDateString(1666325400);
|
||||
expect(dateString).toEqual('2022-10-21');
|
||||
});
|
||||
|
||||
test('it can validate numeric values', () => {
|
||||
expect(isInteger('11')).toEqual(true);
|
||||
expect(isInteger('hey')).toEqual(false);
|
||||
expect(isInteger(' ')).toEqual(false);
|
||||
expect(isInteger('1 2')).toEqual(false);
|
||||
expect(isInteger(2)).toEqual(true);
|
||||
});
|
||||
|
|
|
@ -253,3 +253,7 @@ export const setErrorMessageSafely = (
|
|||
errorMessageSetter({ message: newErrorMessageString });
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isInteger = (str: string | number) => {
|
||||
return /^\d+$/.test(str.toString());
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ import MessageInstanceList from './MessageInstanceList';
|
|||
import Configuration from './Configuration';
|
||||
import JsonSchemaFormBuilder from './JsonSchemaFormBuilder';
|
||||
import ProcessModelNewExperimental from './ProcessModelNewExperimental';
|
||||
import ProcessInstanceFindById from './ProcessInstanceFindById';
|
||||
|
||||
export default function AdminRoutes() {
|
||||
const location = useLocation();
|
||||
|
@ -133,6 +134,10 @@ export default function AdminRoutes() {
|
|||
path="process-models/:process_model_id/form-builder"
|
||||
element={<JsonSchemaFormBuilder />}
|
||||
/>
|
||||
<Route
|
||||
path="process-instances/find-by-id"
|
||||
element={<ProcessInstanceFindById />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react';
|
||||
import { isInteger, modifyProcessIdentifierForPathParam } from '../helpers';
|
||||
import HttpService from '../services/HttpService';
|
||||
import { ProcessInstance } from '../interfaces';
|
||||
|
||||
export default function ProcessInstanceFindById() {
|
||||
const navigate = useNavigate();
|
||||
const [processInstanceId, setProcessInstanceId] = useState<string>('');
|
||||
const [processInstanceIdValid, setProcessInstanceIdValid] =
|
||||
useState<boolean>(true);
|
||||
|
||||
useEffect(() => {}, []);
|
||||
|
||||
const handleProcessInstanceNavigation = (result: any) => {
|
||||
const processInstance: ProcessInstance = result.process_instance;
|
||||
let path = '/admin/process-instances/';
|
||||
if (result.uri_type === 'for-me') {
|
||||
path += 'for-me/';
|
||||
}
|
||||
path += `${modifyProcessIdentifierForPathParam(
|
||||
processInstance.process_model_identifier
|
||||
)}/${processInstance.id}`;
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
const handleFormSubmission = (event: any) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!processInstanceId) {
|
||||
setProcessInstanceIdValid(false);
|
||||
}
|
||||
|
||||
if (processInstanceId && processInstanceIdValid) {
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-instances/find-by-id/${processInstanceId}`,
|
||||
successCallback: handleProcessInstanceNavigation,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleProcessInstanceIdChange = (event: any) => {
|
||||
if (isInteger(event.target.value)) {
|
||||
setProcessInstanceIdValid(true);
|
||||
} else {
|
||||
setProcessInstanceIdValid(false);
|
||||
}
|
||||
setProcessInstanceId(event.target.value);
|
||||
};
|
||||
|
||||
const formElements = () => {
|
||||
return (
|
||||
<TextInput
|
||||
id="process-instance-id-input"
|
||||
invalidText="Process Instance Id must be a number."
|
||||
invalid={!processInstanceIdValid}
|
||||
labelText="Process Instance Id*"
|
||||
value={processInstanceId}
|
||||
onChange={handleProcessInstanceIdChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const formButtons = () => {
|
||||
const buttons = [<Button type="submit">Submit</Button>];
|
||||
return <ButtonSet>{buttons}</ButtonSet>;
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleFormSubmission}>
|
||||
<Stack gap={5}>
|
||||
{formElements()}
|
||||
{formButtons()}
|
||||
</Stack>
|
||||
</Form>
|
||||
);
|
||||
}
|
|
@ -85,6 +85,15 @@ export default function ProcessInstanceList({ variant }: OwnProps) {
|
|||
All
|
||||
</Tab>
|
||||
</Can>
|
||||
<Tab
|
||||
title="Search for a process instance by id."
|
||||
data-qa="process-instance-list-find-by-id"
|
||||
onClick={() => {
|
||||
navigate('/admin/process-instances/find-by-id');
|
||||
}}
|
||||
>
|
||||
Find By Id
|
||||
</Tab>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
<br />
|
||||
|
|
|
@ -100,7 +100,7 @@ export default function ProcessModelShow() {
|
|||
onClose={() => setProcessInstance(null)}
|
||||
>
|
||||
<Link
|
||||
to={`/admin/process-instances/${modifiedProcessModelId}/${processInstance.id}`}
|
||||
to={`/admin/process-instances/for-me/${modifiedProcessModelId}/${processInstance.id}`}
|
||||
data-qa="process-instance-show-link"
|
||||
>
|
||||
view
|
||||
|
|
Loading…
Reference in New Issue