Squashed 'spiffworkflow-backend/' changes from ab9b5f36..7f1776b5

7f1776b5 Merge pull request #161 from sartography/feature/potential_owners_from_task_data
d19477e3 test removing an approver from a list while user lane_owners dict w/ burnettk
2f333204 resolved merge conflicts w/ burnettk
6b9c248b Merge remote-tracking branch 'origin/main' into feature/potential_owners_from_task_data
24a96ccd WIP: deleting user after approval w/ burnettk
8659f212 pyl now passes w/ burnettk
e9ef5bfe Merge remote-tracking branch 'origin/main' into feature/potential_owners_from_task_data
56f8d734 added some support to get the potential task owners from task data w/ burnettk

git-subtree-dir: spiffworkflow-backend
git-subtree-split: 7f1776b5b7f11ae6c02ef435200b0ba87219d7d2
This commit is contained in:
burnettk 2022-10-25 17:38:59 -04:00
parent e228ab4b80
commit d7e8533061
5 changed files with 379 additions and 28 deletions

View File

@ -0,0 +1,26 @@
"""Get_env."""
from typing import Any
from flask import g
from typing import Optional
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from spiffworkflow_backend.scripts.script import Script
class GetUser(Script):
"""GetUser."""
def get_description(self) -> str:
"""Get_description."""
return """Return the current user."""
def run(
self,
task: Optional[SpiffTask],
environment_identifier: str,
*_args: Any,
**kwargs: Any
) -> Any:
"""Run."""
return g.user.username

View File

@ -14,6 +14,7 @@ from typing import List
from typing import NewType
from typing import Optional
from typing import Tuple
from typing import TypedDict
from typing import Union
from flask import current_app
@ -75,6 +76,7 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.task_event import TaskAction
from spiffworkflow_backend.models.task_event import TaskEventModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user import UserModelSchema
from spiffworkflow_backend.scripts.script import Script
from spiffworkflow_backend.services.file_system_service import FileSystemService
@ -106,6 +108,13 @@ DEFAULT_GLOBALS.update(safe_globals)
DEFAULT_GLOBALS["__builtins__"]["__import__"] = _import
class PotentialOwnerIdList(TypedDict):
"""PotentialOwnerIdList."""
potential_owner_ids: list[int]
lane_assignment_id: Optional[int]
class ProcessInstanceProcessorError(Exception):
"""ProcessInstanceProcessorError."""
@ -114,6 +123,10 @@ class NoPotentialOwnersForTaskError(Exception):
"""NoPotentialOwnersForTaskError."""
class PotentialOwnerUserNotFoundError(Exception):
"""PotentialOwnerUserNotFoundError."""
class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
"""This is a custom script processor that can be easily injected into Spiff Workflow.
@ -499,6 +512,58 @@ class ProcessInstanceProcessor:
self.save()
def raise_if_no_potential_owners(
self, potential_owner_ids: list[int], message: str
) -> None:
"""Raise_if_no_potential_owners."""
if not potential_owner_ids:
raise (NoPotentialOwnersForTaskError(message))
def get_potential_owner_ids_from_task(
self, task: SpiffTask
) -> PotentialOwnerIdList:
"""Get_potential_owner_ids_from_task."""
task_spec = task.task_spec
task_lane = "process_initiator"
if task_spec.lane is not None and task_spec.lane != "":
task_lane = task_spec.lane
potential_owner_ids = []
lane_assignment_id = None
if re.match(r"(process.?)initiator", task_lane, re.IGNORECASE):
potential_owner_ids = [self.process_instance_model.process_initiator_id]
elif "lane_owners" in task.data and task_lane in task.data["lane_owners"]:
for username in task.data["lane_owners"][task_lane]:
lane_owner_user = UserModel.query.filter_by(username=username).first()
if lane_owner_user is not None:
potential_owner_ids.append(lane_owner_user.id)
self.raise_if_no_potential_owners(
potential_owner_ids,
f"No users found in task data lane owner list for lane: {task_lane}. "
f"The user list used: {task.data['lane_owners'][task_lane]}",
)
else:
group_model = GroupModel.query.filter_by(identifier=task_lane).first()
if group_model is None:
raise (
NoPotentialOwnersForTaskError(
f"Could not find a group with name matching lane: {task_lane}"
)
)
potential_owner_ids = [
i.user_id for i in group_model.user_group_assignments
]
lane_assignment_id = group_model.id
self.raise_if_no_potential_owners(
potential_owner_ids,
f"Could not find any users in group to assign to lane: {task_lane}",
)
return {
"potential_owner_ids": potential_owner_ids,
"lane_assignment_id": lane_assignment_id,
}
def save(self) -> None:
"""Saves the current state of this processor to the database."""
self.process_instance_model.bpmn_json = self.serialize()
@ -532,32 +597,9 @@ class ProcessInstanceProcessor:
# filter out non-usertasks
task_spec = ready_or_waiting_task.task_spec
if not self.bpmn_process_instance._is_engine_task(task_spec):
ready_or_waiting_task.data["current_user"]["id"]
task_lane = "process_initiator"
if task_spec.lane is not None and task_spec.lane != "":
task_lane = task_spec.lane
potential_owner_ids = []
lane_assignment_id = None
if re.match(r"(process.?)initiator", task_lane, re.IGNORECASE):
potential_owner_ids = [
self.process_instance_model.process_initiator_id
]
else:
group_model = GroupModel.query.filter_by(
identifier=task_lane
).first()
if group_model is None:
raise (
NoPotentialOwnersForTaskError(
f"Could not find a group with name matching lane: {task_lane}"
potential_owner_hash = self.get_potential_owner_ids_from_task(
ready_or_waiting_task
)
)
potential_owner_ids = [
i.user_id for i in group_model.user_group_assignments
]
lane_assignment_id = group_model.id
extensions = ready_or_waiting_task.task_spec.extensions
form_file_name = None
@ -586,12 +628,12 @@ class ProcessInstanceProcessor:
task_title=ready_or_waiting_task.task_spec.description,
task_type=ready_or_waiting_task.task_spec.__class__.__name__,
task_status=ready_or_waiting_task.get_state_name(),
lane_assignment_id=lane_assignment_id,
lane_assignment_id=potential_owner_hash["lane_assignment_id"],
)
db.session.add(active_task)
db.session.commit()
for potential_owner_id in potential_owner_ids:
for potential_owner_id in potential_owner_hash["potential_owner_ids"]:
active_task_user = ActiveTaskUserModel(
user_id=potential_owner_id, active_task_id=active_task.id
)

View File

@ -0,0 +1,175 @@
<?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:collaboration id="Collaboration_0iyw0q7">
<bpmn:participant id="Participant_17eqap4" processRef="Proccess_yhito9d" />
</bpmn:collaboration>
<bpmn:process id="Proccess_yhito9d" isExecutable="true">
<bpmn:laneSet id="LaneSet_17rankp">
<bpmn:lane id="process_initiator" name="Process Initiator">
<bpmn:flowNodeRef>StartEvent_1</bpmn:flowNodeRef>
<bpmn:flowNodeRef>initator_one</bpmn:flowNodeRef>
<bpmn:flowNodeRef>initiator_two</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_0wq6mdd</bpmn:flowNodeRef>
</bpmn:lane>
<bpmn:lane id="finance_team" name="Finance Team">
<bpmn:flowNodeRef>finance_approval_one</bpmn:flowNodeRef>
<bpmn:flowNodeRef>finance_approval_two</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_1s1855p</bpmn:flowNodeRef>
</bpmn:lane>
<bpmn:lane id="bigwig" name="Bigwig">
<bpmn:flowNodeRef>Event_0nsh6vv</bpmn:flowNodeRef>
<bpmn:flowNodeRef>bigwig_approval</bpmn:flowNodeRef>
</bpmn:lane>
</bpmn:laneSet>
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1tbyols</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1tbyols" sourceRef="StartEvent_1" targetRef="initator_one" />
<bpmn:manualTask id="initator_one" name="Initiator One">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>This is initiator user?</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1tbyols</bpmn:incoming>
<bpmn:outgoing>Flow_0xyca1b</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_0jh05kw" sourceRef="initiator_two" targetRef="bigwig_approval" />
<bpmn:sequenceFlow id="Flow_04sc2wb" sourceRef="bigwig_approval" targetRef="Event_0nsh6vv" />
<bpmn:sequenceFlow id="Flow_0xyca1b" sourceRef="initator_one" targetRef="Activity_0wq6mdd" />
<bpmn:manualTask id="initiator_two" name="Initiator Two">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>This is initiator again?</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1aluose</bpmn:incoming>
<bpmn:outgoing>Flow_0jh05kw</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:endEvent id="Event_0nsh6vv">
<bpmn:incoming>Flow_04sc2wb</bpmn:incoming>
</bpmn:endEvent>
<bpmn:manualTask id="bigwig_approval" name="Bigwig Approval">
<bpmn:incoming>Flow_0jh05kw</bpmn:incoming>
<bpmn:outgoing>Flow_04sc2wb</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_13ejjwk" sourceRef="Activity_0wq6mdd" targetRef="finance_approval_one" />
<bpmn:scriptTask id="Activity_0wq6mdd" name="Set Potential Owners For Lanes" scriptFormat="python">
<bpmn:incoming>Flow_0xyca1b</bpmn:incoming>
<bpmn:outgoing>Flow_13ejjwk</bpmn:outgoing>
<bpmn:script>lane_owners = {
"Finance Team": ['testuser3', 'testuser4'],
"Bigwig": ['testadmin1'],
"Process Initiator": ['testadmin1']
}</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1aluose" sourceRef="finance_approval_two" targetRef="initiator_two" />
<bpmn:manualTask id="finance_approval_one" name="Finance Approval One">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>This is finance user one?</spiffworkflow:instructionsForEndUser>
<spiffworkflow:postScript />
<spiffworkflow:preScript />
</bpmn:extensionElements>
<bpmn:incoming>Flow_13ejjwk</bpmn:incoming>
<bpmn:outgoing>Flow_0bgkfue</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:manualTask id="finance_approval_two" name="Finance Approval Two">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>This is finance user two? {{approver}}</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1ivhu7x</bpmn:incoming>
<bpmn:outgoing>Flow_1aluose</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_0bgkfue" sourceRef="finance_approval_one" targetRef="Activity_1s1855p" />
<bpmn:sequenceFlow id="Flow_1ivhu7x" sourceRef="Activity_1s1855p" targetRef="finance_approval_two" />
<bpmn:scriptTask id="Activity_1s1855p" scriptFormat="python">
<bpmn:incoming>Flow_0bgkfue</bpmn:incoming>
<bpmn:outgoing>Flow_1ivhu7x</bpmn:outgoing>
<bpmn:script>approver = get_user()
lane_owners["Finance Team"].remove(approver)</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0iyw0q7">
<bpmndi:BPMNShape id="Participant_17eqap4_di" bpmnElement="Participant_17eqap4" isHorizontal="true">
<dc:Bounds x="129" y="-68" width="600" height="490" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_0ih9iki_di" bpmnElement="bigwig" isHorizontal="true">
<dc:Bounds x="159" y="-68" width="570" height="120" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_0irvyol_di" bpmnElement="finance_team" isHorizontal="true">
<dc:Bounds x="159" y="302" width="570" height="120" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1ewsife_di" bpmnElement="process_initiator" isHorizontal="true">
<dc:Bounds x="159" y="52" width="570" height="250" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1lm1ald_di" bpmnElement="initator_one">
<dc:Bounds x="270" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1c1xxe3_di" bpmnElement="initiator_two">
<dc:Bounds x="550" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0nsh6vv_di" bpmnElement="Event_0nsh6vv">
<dc:Bounds x="672" y="-28" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1i20328_di" bpmnElement="bigwig_approval">
<dc:Bounds x="550" y="-50" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1xdc8g9_di" bpmnElement="Activity_0wq6mdd">
<dc:Bounds x="420" y="140" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1y566d5_di" bpmnElement="finance_approval_one">
<dc:Bounds x="340" y="320" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1rq1fsj_di" bpmnElement="finance_approval_two">
<dc:Bounds x="600" y="320" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0g8uv6m_di" bpmnElement="Activity_1s1855p">
<dc:Bounds x="470" y="320" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1tbyols_di" bpmnElement="Flow_1tbyols">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0jh05kw_di" bpmnElement="Flow_0jh05kw">
<di:waypoint x="600" y="137" />
<di:waypoint x="600" y="30" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_04sc2wb_di" bpmnElement="Flow_04sc2wb">
<di:waypoint x="650" y="-10" />
<di:waypoint x="672" y="-10" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0xyca1b_di" bpmnElement="Flow_0xyca1b">
<di:waypoint x="370" y="179" />
<di:waypoint x="420" y="180" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_13ejjwk_di" bpmnElement="Flow_13ejjwk">
<di:waypoint x="460" y="220" />
<di:waypoint x="460" y="270" />
<di:waypoint x="380" y="270" />
<di:waypoint x="380" y="320" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1aluose_di" bpmnElement="Flow_1aluose">
<di:waypoint x="650" y="320" />
<di:waypoint x="650" y="269" />
<di:waypoint x="600" y="269" />
<di:waypoint x="600" y="217" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0bgkfue_di" bpmnElement="Flow_0bgkfue">
<di:waypoint x="440" y="360" />
<di:waypoint x="470" y="360" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1ivhu7x_di" bpmnElement="Flow_1ivhu7x">
<di:waypoint x="570" y="360" />
<di:waypoint x="600" y="360" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -94,6 +94,8 @@ class TestAuthorizationService(BaseTest):
"""Test_user_can_be_added_to_active_task_on_first_login."""
initiator_user = self.find_or_create_user("initiator_user")
assert initiator_user.principal is not None
# to ensure there is a user that can be assigned to the task
self.find_or_create_user("testuser1")
AuthorizationService.import_permissions_from_yaml_file()
process_model = load_test_spec(

View File

@ -1,5 +1,6 @@
"""Test_process_instance_processor."""
import pytest
from flask import g
from flask.app import Flask
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
@ -118,3 +119,108 @@ class TestProcessInstanceProcessor(BaseTest):
)
assert process_instance.status == ProcessInstanceStatus.complete.value
def test_sets_permission_correctly_on_active_task_when_using_dict(
self,
app: Flask,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_sets_permission_correctly_on_active_task_when_using_dict."""
initiator_user = self.find_or_create_user("initiator_user")
finance_user_three = self.find_or_create_user("testuser3")
finance_user_four = self.find_or_create_user("testuser4")
testadmin1 = self.find_or_create_user("testadmin1")
assert initiator_user.principal is not None
assert finance_user_three.principal is not None
AuthorizationService.import_permissions_from_yaml_file()
finance_group = GroupModel.query.filter_by(identifier="Finance Team").first()
assert finance_group is not None
process_model = load_test_spec(
process_model_id="model_with_lanes",
bpmn_file_name="lanes_with_owner_dict.bpmn",
)
process_instance = self.create_process_instance_from_process_model(
process_model=process_model, user=initiator_user
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
assert active_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 1
assert active_task.potential_owners[0] == initiator_user
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user_three
)
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
assert active_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 2
assert active_task.potential_owners == [finance_user_three, finance_user_four]
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
g.user = finance_user_three
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user_three
)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
assert active_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 1
assert active_task.potential_owners[0] == finance_user_four
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user_four
)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
assert active_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 1
assert active_task.potential_owners[0] == initiator_user
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance
)
with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user
)
ProcessInstanceService.complete_form_task(processor, spiff_task, {}, testadmin1)
assert process_instance.status == ProcessInstanceStatus.complete.value