Merge pull request #95 from sartography/feature/find_by_process_instance_id

Feature/find by process instance
This commit is contained in:
jasquat 2023-01-05 17:31:56 -05:00 committed by GitHub
commit 6dff495aa6
14 changed files with 327 additions and 78 deletions

View File

@ -946,6 +946,27 @@ paths:
schema: schema:
$ref: "#/components/schemas/Workflow" $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}: /process-instances/{modified_process_model_identifier}/{process_instance_id}:
parameters: parameters:
- name: modified_process_model_identifier - name: modified_process_model_identifier

View File

@ -30,6 +30,12 @@ permissions:
allowed_permissions: [read] allowed_permissions: [read]
uri: /* uri: /*
process-instances-find-by-id:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /process-instances/find-by-id/*
tasks-crud: tasks-crud:
groups: [everybody] groups: [everybody]
users: [] users: []

View File

@ -58,6 +58,14 @@ class ProcessModelInfo:
"""Id_for_file_path.""" """Id_for_file_path."""
return self.id.replace("/", os.sep) 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): class ProcessModelInfoSchema(Schema):
"""ProcessModelInfoSchema.""" """ProcessModelInfoSchema."""

View File

@ -31,6 +31,7 @@ from spiffworkflow_backend.models.process_instance_metadata import (
from spiffworkflow_backend.models.process_instance_report import ( from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel, ProcessInstanceReportModel,
) )
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.spec_reference import SpecReferenceNotFoundError from spiffworkflow_backend.models.spec_reference import SpecReferenceNotFoundError
from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel 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 ( from spiffworkflow_backend.routes.process_api_blueprint import (
_un_modify_modified_process_model_id, _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.error_handling_service import ErrorHandlingService
from spiffworkflow_backend.services.git_service import GitCommandError from spiffworkflow_backend.services.git_service import GitCommandError
from spiffworkflow_backend.services.git_service import GitService from spiffworkflow_backend.services.git_service import GitService
@ -88,9 +90,7 @@ def process_instance_run(
do_engine_steps: bool = True, do_engine_steps: bool = True,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_instance_run.""" """Process_instance_run."""
process_instance = ProcessInstanceService().get_process_instance( process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
process_instance_id
)
if process_instance.status != "not_started": if process_instance.status != "not_started":
raise ApiError( raise ApiError(
error_code="process_instance_not_runnable", error_code="process_instance_not_runnable",
@ -138,9 +138,7 @@ def process_instance_terminate(
modified_process_model_identifier: str, modified_process_model_identifier: str,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_instance_run.""" """Process_instance_run."""
process_instance = ProcessInstanceService().get_process_instance( process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
process_instance_id
)
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.terminate() processor.terminate()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
@ -151,9 +149,7 @@ def process_instance_suspend(
modified_process_model_identifier: str, modified_process_model_identifier: str,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_instance_suspend.""" """Process_instance_suspend."""
process_instance = ProcessInstanceService().get_process_instance( process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
process_instance_id
)
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.suspend() processor.suspend()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
@ -164,9 +160,7 @@ def process_instance_resume(
modified_process_model_identifier: str, modified_process_model_identifier: str,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_instance_resume.""" """Process_instance_resume."""
process_instance = ProcessInstanceService().get_process_instance( process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
process_instance_id
)
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.resume() processor.resume()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
@ -575,14 +569,43 @@ def process_instance_reset(
spiff_step: int = 0, spiff_step: int = 0,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Reset a process instance to a particular step.""" """Reset a process instance to a particular step."""
process_instance = ProcessInstanceService().get_process_instance( process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
process_instance_id
)
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.reset_process(spiff_step) processor.reset_process(spiff_step)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") 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( def _get_process_instance(
modified_process_model_identifier: str, modified_process_model_identifier: str,
process_instance: ProcessInstanceModel, process_instance: ProcessInstanceModel,

View File

@ -624,6 +624,84 @@ class AuthorizationService:
return permissions_to_assign 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 @classmethod
def explode_permissions( def explode_permissions(
cls, permission_set: str, target: str cls, permission_set: str, target: str
@ -654,72 +732,20 @@ class AuthorizationService:
permissions = ["create", "read", "update", "delete"] permissions = ["create", "read", "update", "delete"]
if target.startswith("PG:"): if target.startswith("PG:"):
process_group_identifier = ( permissions_to_assign += cls.set_process_group_permissions(
target.removeprefix("PG:").replace("/", ":").removeprefix(":") 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:"): elif target.startswith("PM:"):
process_model_identifier = ( permissions_to_assign += cls.set_process_model_permissions(
target.removeprefix("PM:").replace("/", ":").removeprefix(":") 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": elif permission_set == "start":
raise InvalidPermissionError( raise InvalidPermissionError(
"Permission 'start' is only available for macros PM and PG." "Permission 'start' is only available for macros PM and PG."
) )
elif target.startswith("BASIC"): elif target.startswith("BASIC"):
permissions_to_assign.append( permissions_to_assign += cls.set_basic_permissions()
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/*")
)
elif target == "ALL": elif target == "ALL":
for permission in permissions: for permission in permissions:
permissions_to_assign.append( permissions_to_assign.append(

View File

@ -354,11 +354,8 @@ class BaseTest:
assert has_permission is expected_result assert has_permission is expected_result
def modify_process_identifier_for_path_param(self, identifier: str) -> str: def modify_process_identifier_for_path_param(self, identifier: str) -> str:
"""Identifier.""" """Modify_process_identifier_for_path_param."""
if "\\" in identifier: return ProcessModelInfo.modify_process_identifier_for_path_param(identifier)
raise Exception(f"Found backslash in identifier: {identifier}")
return identifier.replace("/", ":")
def un_modify_modified_process_identifier_for_path_param( def un_modify_modified_process_identifier_for_path_param(
self, modified_identifier: str self, modified_identifier: str

View File

@ -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

View File

@ -277,6 +277,7 @@ class TestAuthorizationService(BaseTest):
) -> None: ) -> None:
"""Test_explode_permissions_basic.""" """Test_explode_permissions_basic."""
expected_permissions = [ expected_permissions = [
("/process-instances/find-by-id/*", "read"),
("/process-instances/for-me", "read"), ("/process-instances/for-me", "read"),
("/process-instances/reports/*", "create"), ("/process-instances/reports/*", "create"),
("/process-instances/reports/*", "delete"), ("/process-instances/reports/*", "delete"),

View File

@ -1,5 +1,6 @@
import { import {
convertSecondsToFormattedDateString, convertSecondsToFormattedDateString,
isInteger,
slugifyString, slugifyString,
underscorizeString, underscorizeString,
} from './helpers'; } from './helpers';
@ -20,3 +21,11 @@ test('it can keep the correct date when converting seconds to date', () => {
const dateString = convertSecondsToFormattedDateString(1666325400); const dateString = convertSecondsToFormattedDateString(1666325400);
expect(dateString).toEqual('2022-10-21'); 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);
});

View File

@ -253,3 +253,7 @@ export const setErrorMessageSafely = (
errorMessageSetter({ message: newErrorMessageString }); errorMessageSetter({ message: newErrorMessageString });
return null; return null;
}; };
export const isInteger = (str: string | number) => {
return /^\d+$/.test(str.toString());
};

View File

@ -23,6 +23,7 @@ import MessageInstanceList from './MessageInstanceList';
import Configuration from './Configuration'; import Configuration from './Configuration';
import JsonSchemaFormBuilder from './JsonSchemaFormBuilder'; import JsonSchemaFormBuilder from './JsonSchemaFormBuilder';
import ProcessModelNewExperimental from './ProcessModelNewExperimental'; import ProcessModelNewExperimental from './ProcessModelNewExperimental';
import ProcessInstanceFindById from './ProcessInstanceFindById';
export default function AdminRoutes() { export default function AdminRoutes() {
const location = useLocation(); const location = useLocation();
@ -133,6 +134,10 @@ export default function AdminRoutes() {
path="process-models/:process_model_id/form-builder" path="process-models/:process_model_id/form-builder"
element={<JsonSchemaFormBuilder />} element={<JsonSchemaFormBuilder />}
/> />
<Route
path="process-instances/find-by-id"
element={<ProcessInstanceFindById />}
/>
</Routes> </Routes>
); );
} }

View File

@ -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>
);
}

View File

@ -85,6 +85,15 @@ export default function ProcessInstanceList({ variant }: OwnProps) {
All All
</Tab> </Tab>
</Can> </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> </TabList>
</Tabs> </Tabs>
<br /> <br />

View File

@ -100,7 +100,7 @@ export default function ProcessModelShow() {
onClose={() => setProcessInstance(null)} onClose={() => setProcessInstance(null)}
> >
<Link <Link
to={`/admin/process-instances/${modifiedProcessModelId}/${processInstance.id}`} to={`/admin/process-instances/for-me/${modifiedProcessModelId}/${processInstance.id}`}
data-qa="process-instance-show-link" data-qa="process-instance-show-link"
> >
view view