updates to disallow modifying a process instance when it is not in the correct state w/ burnettk
This commit is contained in:
parent
aec0416eed
commit
f152195335
|
@ -798,7 +798,7 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/Workflow"
|
||||
|
||||
/process-instances/{modified_process_model_identifier}/{process_instance_id}/terminate:
|
||||
/process-instance-terminate/{modified_process_model_identifier}/{process_instance_id}:
|
||||
parameters:
|
||||
- name: process_instance_id
|
||||
in: path
|
||||
|
@ -819,7 +819,7 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/process-instances/{modified_process_model_identifier}/{process_instance_id}/suspend:
|
||||
/process-instance-suspend/{modified_process_model_identifier}/{process_instance_id}:
|
||||
parameters:
|
||||
- name: process_instance_id
|
||||
in: path
|
||||
|
@ -840,7 +840,7 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/process-instances/{modified_process_model_identifier}/{process_instance_id}/resume:
|
||||
/process-instance-resume/{modified_process_model_identifier}/{process_instance_id}:
|
||||
parameters:
|
||||
- name: process_instance_id
|
||||
in: path
|
||||
|
|
|
@ -30,6 +30,10 @@ class ProcessInstanceTaskDataCannotBeUpdatedError(Exception):
|
|||
"""ProcessInstanceTaskDataCannotBeUpdatedError."""
|
||||
|
||||
|
||||
class ProcessInstanceCannotBeDeletedError(Exception):
|
||||
"""ProcessInstanceCannotBeDeletedError."""
|
||||
|
||||
|
||||
class NavigationItemSchema(Schema):
|
||||
"""NavigationItemSchema."""
|
||||
|
||||
|
@ -135,6 +139,15 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
|
|||
"""Validate_status."""
|
||||
return self.validate_enum_field(key, value, ProcessInstanceStatus)
|
||||
|
||||
def has_terminal_status(self) -> bool:
|
||||
"""Has_terminal_status."""
|
||||
return self.status in self.terminal_statuses()
|
||||
|
||||
@classmethod
|
||||
def terminal_statuses(cls) -> list[str]:
|
||||
"""Terminal_statuses."""
|
||||
return ["complete", "error", "terminated"]
|
||||
|
||||
|
||||
class ProcessInstanceModelSchema(Schema):
|
||||
"""ProcessInstanceModelSchema."""
|
||||
|
|
|
@ -53,6 +53,9 @@ from spiffworkflow_backend.models.principal import PrincipalModel
|
|||
from spiffworkflow_backend.models.process_group import ProcessGroup
|
||||
from spiffworkflow_backend.models.process_group import ProcessGroupSchema
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceApiSchema
|
||||
from spiffworkflow_backend.models.process_instance import (
|
||||
ProcessInstanceCannotBeDeletedError,
|
||||
)
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
|
||||
|
@ -580,6 +583,13 @@ def process_instance_run(
|
|||
process_instance = ProcessInstanceService().get_process_instance(
|
||||
process_instance_id
|
||||
)
|
||||
if process_instance.status != "not_started":
|
||||
raise ApiError(
|
||||
error_code="process_instance_not_runnable",
|
||||
message=f"Process Instance ({process_instance.id}) is currently running or has already run.",
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
|
||||
if do_engine_steps:
|
||||
|
@ -938,7 +948,7 @@ def process_instance_list(
|
|||
|
||||
if report_filter.initiated_by_me is True:
|
||||
process_instance_query = process_instance_query.filter(
|
||||
ProcessInstanceModel.status.in_(["complete", "error", "terminated"]) # type: ignore
|
||||
ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore
|
||||
)
|
||||
process_instance_query = process_instance_query.filter_by(
|
||||
process_initiator=g.user
|
||||
|
@ -947,7 +957,7 @@ def process_instance_list(
|
|||
# TODO: not sure if this is exactly what is wanted
|
||||
if report_filter.with_tasks_completed_by_me is True:
|
||||
process_instance_query = process_instance_query.filter(
|
||||
ProcessInstanceModel.status.in_(["complete", "error", "terminated"]) # type: ignore
|
||||
ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore
|
||||
)
|
||||
# process_instance_query = process_instance_query.join(UserModel, UserModel.id == ProcessInstanceModel.process_initiator_id)
|
||||
# process_instance_query = process_instance_query.add_columns(UserModel.username)
|
||||
|
@ -976,7 +986,7 @@ def process_instance_list(
|
|||
|
||||
if report_filter.with_tasks_completed_by_my_group is True:
|
||||
process_instance_query = process_instance_query.filter(
|
||||
ProcessInstanceModel.status.in_(["complete", "error", "terminated"]) # type: ignore
|
||||
ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore
|
||||
)
|
||||
process_instance_query = process_instance_query.join(
|
||||
SpiffStepDetailsModel,
|
||||
|
@ -1165,6 +1175,12 @@ def process_instance_delete(
|
|||
"""Create_process_instance."""
|
||||
process_instance = find_process_instance_by_id_or_raise(process_instance_id)
|
||||
|
||||
if not process_instance.has_terminal_status():
|
||||
raise ProcessInstanceCannotBeDeletedError(
|
||||
f"Process instance ({process_instance.id}) cannot be deleted since it does not have a terminal status. "
|
||||
f"Current status is {process_instance.status}."
|
||||
)
|
||||
|
||||
# (Pdb) db.session.delete
|
||||
# <bound method delete of <sqlalchemy.orm.scoping.scoped_session object at 0x103eaab30>>
|
||||
db.session.query(SpiffLoggingModel).filter_by(
|
||||
|
|
|
@ -1375,7 +1375,7 @@ class TestProcessApi(BaseTest):
|
|||
assert response.json is not None
|
||||
|
||||
response = client.post(
|
||||
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/terminate",
|
||||
f"/v1.0/process-instance-terminate/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
@ -1396,15 +1396,13 @@ class TestProcessApi(BaseTest):
|
|||
) -> None:
|
||||
"""Test_process_instance_delete."""
|
||||
process_group_id = "my_process_group"
|
||||
process_model_id = "user_task"
|
||||
bpmn_file_name = "user_task.bpmn"
|
||||
bpmn_file_location = "user_task"
|
||||
process_model_id = "sample"
|
||||
bpmn_file_location = "sample"
|
||||
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,
|
||||
)
|
||||
|
||||
|
@ -1420,11 +1418,13 @@ class TestProcessApi(BaseTest):
|
|||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.json is not None
|
||||
assert response.status_code == 200
|
||||
|
||||
delete_response = client.delete(
|
||||
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert delete_response.json["ok"] is True
|
||||
assert delete_response.status_code == 200
|
||||
|
||||
def test_task_show(
|
||||
|
@ -2421,7 +2421,7 @@ class TestProcessApi(BaseTest):
|
|||
assert process_instance.status == "user_input_required"
|
||||
|
||||
client.post(
|
||||
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/suspend",
|
||||
f"/v1.0/process-instance-suspend/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
process_instance = ProcessInstanceService().get_process_instance(
|
||||
|
@ -2429,15 +2429,25 @@ class TestProcessApi(BaseTest):
|
|||
)
|
||||
assert process_instance.status == "suspended"
|
||||
|
||||
# TODO: Why can I run a suspended process instance?
|
||||
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),
|
||||
)
|
||||
process_instance = ProcessInstanceService().get_process_instance(
|
||||
process_instance_id
|
||||
)
|
||||
assert process_instance.status == "suspended"
|
||||
assert response.status_code == 400
|
||||
|
||||
# task = response.json['next_task']
|
||||
|
||||
print("test_process_instance_suspend")
|
||||
response = client.post(
|
||||
f"/v1.0/process-instance-resume/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
process_instance = ProcessInstanceService().get_process_instance(
|
||||
process_instance_id
|
||||
)
|
||||
assert process_instance.status == "waiting"
|
||||
|
||||
def test_script_unit_test_run(
|
||||
self,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export default class ProcessInstanceClass {
|
||||
static terminalStatuses() {
|
||||
return ['complete', 'error', 'terminated'];
|
||||
}
|
||||
}
|
|
@ -11,6 +11,9 @@ export const useUriListForPermissions = () => {
|
|||
processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`,
|
||||
processInstanceCreatePath: `/v1.0/process-instances/${params.process_model_id}`,
|
||||
processInstanceActionPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}`,
|
||||
processInstanceResumePath: `/v1.0/process-instance-resume/${params.process_model_id}/${params.process_instance_id}`,
|
||||
processInstanceSuspendPath: `/v1.0/process-instance-suspend/${params.process_model_id}/${params.process_instance_id}`,
|
||||
processInstanceTerminatePath: `/v1.0/process-instance-terminate/${params.process_model_id}/${params.process_instance_id}`,
|
||||
processInstanceListPath: '/v1.0/process-instances',
|
||||
processInstanceLogListPath: `/v1.0/logs/${params.process_model_id}/${params.process_instance_id}`,
|
||||
processInstanceReportListPath: '/v1.0/process-instances/reports',
|
||||
|
|
|
@ -45,6 +45,7 @@ import {
|
|||
ProcessInstanceTask,
|
||||
} from '../interfaces';
|
||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
import ProcessInstanceClass from '../classes/ProcessInstanceClass';
|
||||
|
||||
export default function ProcessInstanceShow() {
|
||||
const navigate = useNavigate();
|
||||
|
@ -74,9 +75,9 @@ export default function ProcessInstanceShow() {
|
|||
[targetUris.processInstanceActionPath]: ['DELETE'],
|
||||
[targetUris.processInstanceLogListPath]: ['GET'],
|
||||
[targetUris.processModelShowPath]: ['PUT'],
|
||||
[`${targetUris.processInstanceActionPath}/suspend`]: ['POST'],
|
||||
[`${targetUris.processInstanceActionPath}/terminate`]: ['POST'],
|
||||
[`${targetUris.processInstanceActionPath}/resume`]: ['POST'],
|
||||
[`${targetUris.processInstanceResumePath}`]: ['POST'],
|
||||
[`${targetUris.processInstanceSuspendPath}`]: ['POST'],
|
||||
[`${targetUris.processInstanceTerminatePath}`]: ['POST'],
|
||||
};
|
||||
const { ability, permissionsLoaded } = usePermissionFetcher(
|
||||
permissionRequestData
|
||||
|
@ -146,7 +147,7 @@ export default function ProcessInstanceShow() {
|
|||
|
||||
const terminateProcessInstance = () => {
|
||||
HttpService.makeCallToBackend({
|
||||
path: `${targetUris.processInstanceActionPath}/terminate`,
|
||||
path: `${targetUris.processInstanceTerminatePath}`,
|
||||
successCallback: refreshPage,
|
||||
httpMethod: 'POST',
|
||||
});
|
||||
|
@ -154,7 +155,7 @@ export default function ProcessInstanceShow() {
|
|||
|
||||
const suspendProcessInstance = () => {
|
||||
HttpService.makeCallToBackend({
|
||||
path: `${targetUris.processInstanceActionPath}/suspend`,
|
||||
path: `${targetUris.processInstanceSuspendPath}`,
|
||||
successCallback: refreshPage,
|
||||
httpMethod: 'POST',
|
||||
});
|
||||
|
@ -162,7 +163,7 @@ export default function ProcessInstanceShow() {
|
|||
|
||||
const resumeProcessInstance = () => {
|
||||
HttpService.makeCallToBackend({
|
||||
path: `${targetUris.processInstanceActionPath}/resume`,
|
||||
path: `${targetUris.processInstanceResumePath}`,
|
||||
successCallback: refreshPage,
|
||||
httpMethod: 'POST',
|
||||
});
|
||||
|
@ -333,7 +334,7 @@ export default function ProcessInstanceShow() {
|
|||
const terminateButton = () => {
|
||||
if (
|
||||
processInstance &&
|
||||
!['complete', 'terminated', 'error'].includes(processInstance.status)
|
||||
!ProcessInstanceClass.terminalStatuses().includes(processInstance.status)
|
||||
) {
|
||||
return (
|
||||
<ButtonWithConfirmation
|
||||
|
@ -353,9 +354,9 @@ export default function ProcessInstanceShow() {
|
|||
const suspendButton = () => {
|
||||
if (
|
||||
processInstance &&
|
||||
!['complete', 'terminated', 'error', 'suspended'].includes(
|
||||
processInstance.status
|
||||
)
|
||||
!ProcessInstanceClass.terminalStatuses()
|
||||
.concat(['suspended'])
|
||||
.includes(processInstance.status)
|
||||
) {
|
||||
return (
|
||||
<Button
|
||||
|
@ -608,21 +609,23 @@ export default function ProcessInstanceShow() {
|
|||
};
|
||||
|
||||
const buttonIcons = () => {
|
||||
if (!processInstance) {
|
||||
return null;
|
||||
}
|
||||
const elements = [];
|
||||
if (
|
||||
ability.can('POST', `${targetUris.processInstanceActionPath}/terminate`)
|
||||
) {
|
||||
if (ability.can('POST', `${targetUris.processInstanceTerminatePath}`)) {
|
||||
elements.push(terminateButton());
|
||||
}
|
||||
if (
|
||||
ability.can('POST', `${targetUris.processInstanceActionPath}/suspend`)
|
||||
) {
|
||||
if (ability.can('POST', `${targetUris.processInstanceSuspendPath}`)) {
|
||||
elements.push(suspendButton());
|
||||
}
|
||||
if (ability.can('POST', `${targetUris.processInstanceActionPath}/resume`)) {
|
||||
if (ability.can('POST', `${targetUris.processInstanceResumePath}`)) {
|
||||
elements.push(resumeButton());
|
||||
}
|
||||
if (ability.can('DELETE', targetUris.processInstanceActionPath)) {
|
||||
if (
|
||||
ability.can('DELETE', targetUris.processInstanceActionPath) &&
|
||||
ProcessInstanceClass.terminalStatuses().includes(processInstance.status)
|
||||
) {
|
||||
elements.push(
|
||||
<ButtonWithConfirmation
|
||||
data-qa="process-instance-delete"
|
||||
|
@ -630,7 +633,7 @@ export default function ProcessInstanceShow() {
|
|||
renderIcon={TrashCan}
|
||||
iconDescription="Delete"
|
||||
hasIconOnly
|
||||
description={`Delete Process Instance: ${processInstance}`}
|
||||
description={`Delete Process Instance: ${processInstance.id}`}
|
||||
onConfirmation={deleteProcessInstance}
|
||||
confirmButtonLabel="Delete"
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue