added some code to respect lanes in a process model w/ burnettk

This commit is contained in:
jasquat 2022-10-20 17:06:48 -04:00
parent d452208efe
commit a387b78786
18 changed files with 319 additions and 45 deletions

View File

@ -6,6 +6,7 @@ import pytest
from flask.app import Flask from flask.app import Flask
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from flask_bpmn.models.db import SpiffworkflowBaseDBModel from flask_bpmn.models.db import SpiffworkflowBaseDBModel
from spiffworkflow_backend.models.active_task_user import ActiveTaskUserModel
from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
@ -56,6 +57,8 @@ def app() -> Flask:
@pytest.fixture() @pytest.fixture()
def with_db_and_bpmn_file_cleanup() -> None: def with_db_and_bpmn_file_cleanup() -> None:
"""Process_group_resource.""" """Process_group_resource."""
db.session.query(ActiveTaskUserModel).delete()
for model in SpiffworkflowBaseDBModel._all_subclasses(): for model in SpiffworkflowBaseDBModel._all_subclasses():
db.session.query(model).delete() db.session.query(model).delete()
db.session.commit() db.session.commit()

View File

@ -1,3 +1,5 @@
from __future__ import with_statement
import logging import logging
from logging.config import fileConfig from logging.config import fileConfig

View File

@ -1,8 +1,8 @@
"""empty message """empty message
Revision ID: cf862b761896 Revision ID: 1e25676fed79
Revises: Revises:
Create Date: 2022-10-20 11:23:58.758922 Create Date: 2022-10-20 16:29:56.969687
""" """
from alembic import op from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'cf862b761896' revision = '1e25676fed79'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -157,7 +157,8 @@ def upgrade():
op.create_table('active_task', op.create_table('active_task',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('process_instance_id', sa.Integer(), nullable=False), sa.Column('process_instance_id', sa.Integer(), nullable=False),
sa.Column('assigned_principal_id', sa.Integer(), nullable=True), sa.Column('actual_owner_id', sa.Integer(), nullable=True),
sa.Column('lane_assignment_id', sa.Integer(), nullable=True),
sa.Column('form_file_name', sa.String(length=50), nullable=True), sa.Column('form_file_name', sa.String(length=50), nullable=True),
sa.Column('ui_form_file_name', sa.String(length=50), nullable=True), sa.Column('ui_form_file_name', sa.String(length=50), nullable=True),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True),
@ -169,7 +170,8 @@ def upgrade():
sa.Column('task_status', sa.String(length=50), nullable=True), sa.Column('task_status', sa.String(length=50), nullable=True),
sa.Column('process_model_display_name', sa.String(length=255), nullable=True), sa.Column('process_model_display_name', sa.String(length=255), nullable=True),
sa.Column('task_data', sa.Text(length=4294000000), nullable=True), sa.Column('task_data', sa.Text(length=4294000000), nullable=True),
sa.ForeignKeyConstraint(['assigned_principal_id'], ['principal.id'], ), sa.ForeignKeyConstraint(['actual_owner_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['lane_assignment_id'], ['group.id'], ),
sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ), sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('task_id', 'process_instance_id', name='active_task_unique') sa.UniqueConstraint('task_id', 'process_instance_id', name='active_task_unique')
@ -272,6 +274,17 @@ def upgrade():
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id') sa.PrimaryKeyConstraint('id')
) )
op.create_table('active_task_user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('active_task_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['active_task_id'], ['active_task.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('active_task_id', 'user_id', name='active_task_user_unique')
)
op.create_index(op.f('ix_active_task_user_active_task_id'), 'active_task_user', ['active_task_id'], unique=False)
op.create_index(op.f('ix_active_task_user_user_id'), 'active_task_user', ['user_id'], unique=False)
op.create_table('data_store', op.create_table('data_store',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True),
@ -305,6 +318,9 @@ def downgrade():
op.drop_index(op.f('ix_message_correlation_message_instance_message_correlation_id'), table_name='message_correlation_message_instance') op.drop_index(op.f('ix_message_correlation_message_instance_message_correlation_id'), table_name='message_correlation_message_instance')
op.drop_table('message_correlation_message_instance') op.drop_table('message_correlation_message_instance')
op.drop_table('data_store') op.drop_table('data_store')
op.drop_index(op.f('ix_active_task_user_user_id'), table_name='active_task_user')
op.drop_index(op.f('ix_active_task_user_active_task_id'), table_name='active_task_user')
op.drop_table('active_task_user')
op.drop_table('task_event') op.drop_table('task_event')
op.drop_table('spiff_logging') op.drop_table('spiff_logging')
op.drop_table('permission_assignment') op.drop_table('permission_assignment')

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get( SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get(
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="staging.yml" "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="development.yml"
) )
SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get( SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get(

View File

@ -2,8 +2,8 @@ groups:
admin: admin:
users: [testadmin1, testadmin2] users: [testadmin1, testadmin2]
finance: Finance Team:
users: [testuser1, testuser2] users: [testuser2]
hr: hr:
users: [testuser2, testuser3, testuser4] users: [testuser2, testuser3, testuser4]
@ -16,13 +16,13 @@ permissions:
uri: /* uri: /*
read-all: read-all:
groups: [finance, hr, admin] groups: ["Finance Team", hr, admin]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /* uri: /*
finance-admin: finance-admin:
groups: [finance] groups: ["Finance Team"]
users: [testuser4] users: [testuser4]
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/finance/* uri: /v1.0/process-groups/finance/*

View File

@ -15,8 +15,8 @@ from spiffworkflow_backend.models.user_group_assignment import (
UserGroupAssignmentModel, UserGroupAssignmentModel,
) # noqa: F401 ) # noqa: F401
from spiffworkflow_backend.models.active_task import ActiveTaskModel # noqa: F401 from spiffworkflow_backend.models.active_task import ActiveTaskModel # noqa: F401
from spiffworkflow_backend.models.active_task_user import (ActiveTaskUserModel)
from spiffworkflow_backend.models.bpmn_process_id_lookup import ( from spiffworkflow_backend.models.bpmn_process_id_lookup import (
BpmnProcessIdLookup, BpmnProcessIdLookup,
) # noqa: F401 ) # noqa: F401

View File

@ -3,16 +3,25 @@ from __future__ import annotations
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from flask_bpmn.models.db import SpiffworkflowBaseDBModel from flask_bpmn.models.db import SpiffworkflowBaseDBModel
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.orm import RelationshipProperty from sqlalchemy.orm import RelationshipProperty
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.principal import PrincipalModel from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.task import Task from spiffworkflow_backend.models.task import Task
from spiffworkflow_backend.models.user import UserModel
if TYPE_CHECKING:
from spiffworkflow_backend.models.active_task_user import (
ActiveTaskUserModel,
) # noqa: F401
@dataclass @dataclass
@ -26,14 +35,15 @@ class ActiveTaskModel(SpiffworkflowBaseDBModel):
), ),
) )
assigned_principal: RelationshipProperty[PrincipalModel] = relationship( actual_owner: RelationshipProperty[UserModel] = relationship(
PrincipalModel UserModel
) )
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(db.Integer, primary_key=True)
process_instance_id: int = db.Column( process_instance_id: int = db.Column(
ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore
) )
assigned_principal_id: int = db.Column(ForeignKey(PrincipalModel.id)) actual_owner_id: int = db.Column(ForeignKey(UserModel.id))
lane_assignment_id: int | None = db.Column(ForeignKey(GroupModel.id))
form_file_name: str | None = db.Column(db.String(50)) form_file_name: str | None = db.Column(db.String(50))
ui_form_file_name: str | None = db.Column(db.String(50)) ui_form_file_name: str | None = db.Column(db.String(50))
@ -48,6 +58,14 @@ class ActiveTaskModel(SpiffworkflowBaseDBModel):
process_model_display_name = db.Column(db.String(255)) process_model_display_name = db.Column(db.String(255))
task_data: str = db.Column(db.Text(4294000000)) task_data: str = db.Column(db.Text(4294000000))
active_task_users = relationship("ActiveTaskUserModel", cascade="delete")
potential_owners = relationship( # type: ignore
"UserModel",
viewonly=True,
secondary="active_task_user",
overlaps="active_task_user,users",
)
@classmethod @classmethod
def to_task(cls, task: ActiveTaskModel) -> Task: def to_task(cls, task: ActiveTaskModel) -> Task:
"""To_task.""" """To_task."""

View File

@ -0,0 +1,31 @@
from __future__ import annotations
from dataclasses import dataclass
from flask_bpmn.models.db import db
from flask_bpmn.models.db import SpiffworkflowBaseDBModel
from sqlalchemy import ForeignKey
from spiffworkflow_backend.models.active_task import ActiveTaskModel
from spiffworkflow_backend.models.user import UserModel
@dataclass
class ActiveTaskUserModel(SpiffworkflowBaseDBModel):
__tablename__ = "active_task_user"
__table_args__ = (
db.UniqueConstraint(
"active_task_id",
"user_id",
name="active_task_user_unique",
),
)
id = db.Column(db.Integer, primary_key=True)
active_task_id = db.Column(
ForeignKey(ActiveTaskModel.id), nullable=False, index=True # type: ignore
)
user_id = db.Column(
ForeignKey(UserModel.id), nullable=False, index=True
)

View File

@ -1,4 +1,5 @@
"""User.""" """User."""
from __future__ import annotations
from typing import Any from typing import Any
import jwt import jwt

View File

@ -886,7 +886,7 @@ def process_instance_report_show(
def task_list_my_tasks(page: int = 1, per_page: int = 100) -> flask.wrappers.Response: def task_list_my_tasks(page: int = 1, per_page: int = 100) -> flask.wrappers.Response:
"""Task_list_my_tasks.""" """Task_list_my_tasks."""
principal = find_principal_or_raise() principal = find_principal_or_raise()
# TODO: use join table
active_tasks = ( active_tasks = (
ActiveTaskModel.query.filter_by(assigned_principal_id=principal.id) ActiveTaskModel.query.filter_by(assigned_principal_id=principal.id)
.order_by(desc(ActiveTaskModel.id)) # type: ignore .order_by(desc(ActiveTaskModel.id)) # type: ignore
@ -1089,6 +1089,7 @@ def task_submit(
ProcessInstanceService.update_task_assignments(processor) ProcessInstanceService.update_task_assignments(processor)
# TODO: update
next_active_task_assigned_to_me = ActiveTaskModel.query.filter_by( next_active_task_assigned_to_me = ActiveTaskModel.query.filter_by(
assigned_principal_id=principal.id, process_instance_id=process_instance.id assigned_principal_id=principal.id, process_instance_id=process_instance.id
).first() ).first()
@ -1258,10 +1259,12 @@ def find_active_task_by_id_or_raise(
process_instance_id: int, task_id: str, principal_id: PrincipalModel process_instance_id: int, task_id: str, principal_id: PrincipalModel
) -> ActiveTaskModel: ) -> ActiveTaskModel:
"""Find_active_task_by_id_or_raise.""" """Find_active_task_by_id_or_raise."""
# TODO: update
active_task_assigned_to_me = ActiveTaskModel.query.filter_by( active_task_assigned_to_me = ActiveTaskModel.query.filter_by(
process_instance_id=process_instance_id, process_instance_id=process_instance_id,
task_id=task_id, task_id=task_id,
assigned_principal_id=principal_id, # assigned_principal_id=principal_id,
).first() ).first()
if active_task_assigned_to_me is None: if active_task_assigned_to_me is None:
message = ( message = (

View File

@ -164,14 +164,16 @@ class AuthorizationService:
) )
if "users" in permission_config: if "users" in permission_config:
for username in permission_config["users"]: for username in permission_config["users"]:
principal = ( user = UserModel.query.filter_by(username=username).first()
PrincipalModel.query.join(UserModel) if user is not None:
.filter(UserModel.username == username) principal = (
.first() PrincipalModel.query.join(UserModel)
) .filter(UserModel.username == username)
cls.create_permission_for_principal( .first()
principal, permission_target, allowed_permission )
) cls.create_permission_for_principal(
principal, permission_target, allowed_permission
)
@classmethod @classmethod
def create_permission_for_principal( def create_permission_for_principal(

View File

@ -4,6 +4,7 @@ import decimal
import json import json
import logging import logging
import os import os
import re
import time import time
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
@ -55,9 +56,11 @@ from SpiffWorkflow.task import TaskState
from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
from spiffworkflow_backend.models.active_task import ActiveTaskModel from spiffworkflow_backend.models.active_task import ActiveTaskModel
from spiffworkflow_backend.models.active_task_user import ActiveTaskUserModel
from spiffworkflow_backend.models.bpmn_process_id_lookup import BpmnProcessIdLookup from spiffworkflow_backend.models.bpmn_process_id_lookup import BpmnProcessIdLookup
from spiffworkflow_backend.models.file import File from spiffworkflow_backend.models.file import File
from spiffworkflow_backend.models.file import FileType from spiffworkflow_backend.models.file import FileType
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel from spiffworkflow_backend.models.message_correlation import MessageCorrelationModel
from spiffworkflow_backend.models.message_correlation_message_instance import ( from spiffworkflow_backend.models.message_correlation_message_instance import (
MessageCorrelationMessageInstanceModel, MessageCorrelationMessageInstanceModel,
@ -108,6 +111,10 @@ class ProcessInstanceProcessorError(Exception):
"""ProcessInstanceProcessorError.""" """ProcessInstanceProcessorError."""
class NoPotentialOwnersForTaskError(Exception):
pass
class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
"""This is a custom script processor that can be easily injected into Spiff Workflow. """This is a custom script processor that can be easily injected into Spiff Workflow.
@ -511,28 +518,47 @@ class ProcessInstanceProcessor:
if self.bpmn_process_instance.is_completed(): if self.bpmn_process_instance.is_completed():
self.process_instance_model.end_in_seconds = round(time.time()) self.process_instance_model.end_in_seconds = round(time.time())
db.session.add(self.process_instance_model)
ActiveTaskModel.query.filter_by( active_tasks = ActiveTaskModel.query.filter_by(
process_instance_id=self.process_instance_model.id process_instance_id=self.process_instance_model.id
).delete() ).all()
if len(active_tasks) > 0:
for at in active_tasks:
db.session.delete(at)
db.session.add(self.process_instance_model)
db.session.commit()
ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks() ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks()
for ready_or_waiting_task in ready_or_waiting_tasks: for ready_or_waiting_task in ready_or_waiting_tasks:
# filter out non-usertasks # filter out non-usertasks
if not self.bpmn_process_instance._is_engine_task( task_spec = ready_or_waiting_task.task_spec
ready_or_waiting_task.task_spec if not self.bpmn_process_instance._is_engine_task(task_spec):
):
user_id = ready_or_waiting_task.data["current_user"]["id"] user_id = ready_or_waiting_task.data["current_user"]["id"]
principal = PrincipalModel.query.filter_by(user_id=user_id).first() # principal = PrincipalModel.query.filter_by(user_id=user_id).first()
if principal is None: # if principal is None:
raise ( # raise (
ApiError( # ApiError(
error_code="principal_not_found", # error_code="principal_not_found",
message=f"Principal not found from user id: {user_id}", # message=f"Principal not found from user id: {user_id}",
status_code=400, # status_code=400,
) # )
) # )
# import pdb; pdb.set_trace()
task_lane = 'process_initiator'
if task_spec.lane is not None:
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_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 extensions = ready_or_waiting_task.task_spec.extensions
@ -555,7 +581,6 @@ class ProcessInstanceProcessor:
active_task = ActiveTaskModel( active_task = ActiveTaskModel(
process_instance_id=self.process_instance_model.id, process_instance_id=self.process_instance_model.id,
process_model_display_name=process_model_display_name, process_model_display_name=process_model_display_name,
assigned_principal_id=principal.id,
form_file_name=form_file_name, form_file_name=form_file_name,
ui_form_file_name=ui_form_file_name, ui_form_file_name=ui_form_file_name,
task_id=str(ready_or_waiting_task.id), task_id=str(ready_or_waiting_task.id),
@ -564,10 +589,15 @@ class ProcessInstanceProcessor:
task_type=ready_or_waiting_task.task_spec.__class__.__name__, task_type=ready_or_waiting_task.task_spec.__class__.__name__,
task_status=ready_or_waiting_task.get_state_name(), task_status=ready_or_waiting_task.get_state_name(),
task_data=json.dumps(ready_or_waiting_task.data), task_data=json.dumps(ready_or_waiting_task.data),
lane_assignment_id=lane_assignment_id,
) )
db.session.add(active_task) db.session.add(active_task)
db.session.commit()
db.session.commit() for potential_owner_id in potential_owner_ids:
active_task_user = ActiveTaskUserModel(user_id=potential_owner_id,active_task_id=active_task.id)
db.session.add(active_task_user)
db.session.commit()
@staticmethod @staticmethod
def get_parser() -> MyCustomParser: def get_parser() -> MyCustomParser:

View File

@ -282,8 +282,7 @@ class ProcessInstanceService:
ProcessInstanceService.log_task_action( ProcessInstanceService.log_task_action(
user.id, processor, spiff_task, TaskAction.COMPLETE.value user.id, processor, spiff_task, TaskAction.COMPLETE.value
) )
processor.do_engine_steps() processor.do_engine_steps(save=True)
processor.save()
@staticmethod @staticmethod
def log_task_action( def log_task_action(

View File

@ -0,0 +1,103 @@
<?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>Event_06f4e68</bpmn:flowNodeRef>
<bpmn:flowNodeRef>initiator_two</bpmn:flowNodeRef>
</bpmn:lane>
<bpmn:lane id="finance_team" name="Finance Team">
<bpmn:flowNodeRef>finance_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:sequenceFlow id="Flow_16ppta1" sourceRef="initator_one" targetRef="finance_approval" />
<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_16ppta1</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:manualTask id="finance_approval" name="Finance Approval">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>This is finance user?</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_16ppta1</bpmn:incoming>
<bpmn:outgoing>Flow_1cfcauf</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_1cfcauf" sourceRef="finance_approval" targetRef="initiator_two" />
<bpmn:endEvent id="Event_06f4e68">
<bpmn:incoming>Flow_0x92f7d</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0x92f7d" sourceRef="initiator_two" targetRef="Event_06f4e68" />
<bpmn:manualTask id="initiator_two" name="Initiator Two">
<bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>This is initiator again?</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1cfcauf</bpmn:incoming>
<bpmn:outgoing>Flow_0x92f7d</bpmn:outgoing>
</bpmn:manualTask>
</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="52" width="600" height="370" />
</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_1y566d5_di" bpmnElement="finance_approval">
<dc:Bounds x="310" y="320" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_06f4e68_di" bpmnElement="Event_06f4e68">
<dc:Bounds x="572" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1c1xxe3_di" bpmnElement="initiator_two">
<dc:Bounds x="440" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</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_16ppta1_di" bpmnElement="Flow_16ppta1">
<di:waypoint x="320" y="217" />
<di:waypoint x="320" y="269" />
<di:waypoint x="360" y="269" />
<di:waypoint x="360" y="320" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1cfcauf_di" bpmnElement="Flow_1cfcauf">
<di:waypoint x="410" y="360" />
<di:waypoint x="425" y="360" />
<di:waypoint x="425" y="177" />
<di:waypoint x="440" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0x92f7d_di" bpmnElement="Flow_0x92f7d">
<di:waypoint x="540" y="177" />
<di:waypoint x="572" y="177" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -207,10 +207,12 @@ class BaseTest:
# return public_access_token # return public_access_token
def create_process_instance_from_process_model( def create_process_instance_from_process_model(
self, process_model: ProcessModelInfo, status: Optional[str] = "not_started" self, process_model: ProcessModelInfo, status: Optional[str] = "not_started", user: Optional[UserModel] = None
) -> ProcessInstanceModel: ) -> ProcessInstanceModel:
"""Create_process_instance_from_process_model.""" """Create_process_instance_from_process_model."""
user = self.find_or_create_user() if user is None:
user = self.find_or_create_user()
current_time = round(time.time()) current_time = round(time.time())
process_instance = ProcessInstanceModel( process_instance = ProcessInstanceModel(
status=status, status=status,

View File

@ -19,6 +19,11 @@ class TestAuthorizationService(BaseTest):
raise_if_missing_user=True raise_if_missing_user=True
) )
def test_does_not_fail_if_user_not_created(
self, app: Flask, with_db_and_bpmn_file_cleanup: None
) -> None:
AuthorizationService.import_permissions_from_yaml_file()
def test_can_import_permissions_from_yaml( def test_can_import_permissions_from_yaml(
self, app: Flask, with_db_and_bpmn_file_cleanup: None self, app: Flask, with_db_and_bpmn_file_cleanup: None
) -> None: ) -> None:

View File

@ -1,6 +1,11 @@
"""Test_process_instance_processor.""" """Test_process_instance_processor."""
from flask.app import Flask from flask.app import Flask
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel, ProcessInstanceStatus
from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend.services.process_instance_processor import ( from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor, ProcessInstanceProcessor,
@ -34,3 +39,57 @@ class TestProcessInstanceProcessor(BaseTest):
result result
== "Chuck Norris doesnt read books. He stares them down until he gets the information he wants." == "Chuck Norris doesnt read books. He stares them down until he gets the information he wants."
) )
def test_sets_permission_correctly_on_active_task(
self,
app: Flask,
with_db_and_bpmn_file_cleanup: None,
) -> None:
testuser1 = self.find_or_create_user("testuser1")
testuser2 = self.find_or_create_user("testuser2")
assert testuser1.principal is not None
assert testuser2.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.bpmn")
process_instance = self.create_process_instance_from_process_model(process_model=process_model, user=testuser1)
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] == testuser1
task = processor.__class__.get_task_by_bpmn_identifier(active_task.task_name, processor.bpmn_process_instance)
ProcessInstanceService.complete_form_task(
processor, task, {}, testuser1
)
assert len(process_instance.active_tasks) == 1
active_task = process_instance.active_tasks[0]
assert active_task.lane_assignment_id == finance_group.id
assert len(active_task.potential_owners) == 1
assert active_task.potential_owners[0] == testuser2
task = processor.__class__.get_task_by_bpmn_identifier(active_task.task_name, processor.bpmn_process_instance)
ProcessInstanceService.complete_form_task(
processor, task, {}, testuser1
)
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] == testuser1
task = processor.__class__.get_task_by_bpmn_identifier(active_task.task_name, processor.bpmn_process_instance)
ProcessInstanceService.complete_form_task(
processor, task, {}, testuser1
)
assert process_instance.status == ProcessInstanceStatus.complete.value