Merge remote-tracking branch 'origin/main' into bug/cypress-tests

This commit is contained in:
burnettk 2022-12-20 22:05:21 -05:00
commit cec5679dcf
80 changed files with 2848 additions and 1442 deletions

View File

@ -81,17 +81,18 @@ class EventDefinitionParser(TaskParser):
"""Parse the timerEventDefinition node and return an instance of TimerEventDefinition.""" """Parse the timerEventDefinition node and return an instance of TimerEventDefinition."""
try: try:
label = self.node.get('name', self.node.get('id'))
time_date = first(self.xpath('.//bpmn:timeDate')) time_date = first(self.xpath('.//bpmn:timeDate'))
if time_date is not None: if time_date is not None:
return TimerEventDefinition(self.node.get('name'), time_date.text) return TimerEventDefinition(label, time_date.text)
time_duration = first(self.xpath('.//bpmn:timeDuration')) time_duration = first(self.xpath('.//bpmn:timeDuration'))
if time_duration is not None: if time_duration is not None:
return TimerEventDefinition(self.node.get('name'), time_duration.text) return TimerEventDefinition(label, time_duration.text)
time_cycle = first(self.xpath('.//bpmn:timeCycle')) time_cycle = first(self.xpath('.//bpmn:timeCycle'))
if time_cycle is not None: if time_cycle is not None:
return CycleTimerEventDefinition(self.node.get('name'), time_cycle.text) return CycleTimerEventDefinition(label, time_cycle.text)
raise ValidationException("Unknown Time Specification", node=self.node, filename=self.filename) raise ValidationException("Unknown Time Specification", node=self.node, filename=self.filename)
except Exception as e: except Exception as e:
raise ValidationException("Time Specification Error. " + str(e), node=self.node, filename=self.filename) raise ValidationException("Time Specification Error. " + str(e), node=self.node, filename=self.filename)

View File

@ -156,6 +156,9 @@ class EventBasedGateway(CatchingEvent):
def spec_type(self): def spec_type(self):
return 'Event Based Gateway' return 'Event Based Gateway'
def _predict_hook(self, my_task):
my_task._sync_children(self.outputs, state=TaskState.MAYBE)
def _on_complete_hook(self, my_task): def _on_complete_hook(self, my_task):
for child in my_task.children: for child in my_task.children:
if not child.task_spec.event_definition.has_fired(child): if not child.task_spec.event_definition.has_fired(child):

View File

@ -20,6 +20,7 @@
import datetime import datetime
from copy import deepcopy from copy import deepcopy
from SpiffWorkflow.task import TaskState
class EventDefinition(object): class EventDefinition(object):
""" """
@ -307,6 +308,11 @@ class TimerEventDefinition(EventDefinition):
The Timer is considered to have fired if the evaluated dateTime The Timer is considered to have fired if the evaluated dateTime
expression is before datetime.datetime.now() expression is before datetime.datetime.now()
""" """
if my_task.internal_data.get('event_fired'):
# If we manually send this event, this will be set
return True
dt = my_task.workflow.script_engine.evaluate(my_task, self.dateTime) dt = my_task.workflow.script_engine.evaluate(my_task, self.dateTime)
if isinstance(dt,datetime.timedelta): if isinstance(dt,datetime.timedelta):
if my_task._get_internal_data('start_time',None) is not None: if my_task._get_internal_data('start_time',None) is not None:
@ -330,6 +336,9 @@ class TimerEventDefinition(EventDefinition):
now = datetime.date.today() now = datetime.date.today()
return now > dt return now > dt
def __eq__(self, other):
return self.__class__.__name__ == other.__class__.__name__ and self.label == other.label
def serialize(self): def serialize(self):
retdict = super(TimerEventDefinition, self).serialize() retdict = super(TimerEventDefinition, self).serialize()
retdict['label'] = self.label retdict['label'] = self.label
@ -363,6 +372,10 @@ class CycleTimerEventDefinition(EventDefinition):
# We will fire this timer whenever a cycle completes # We will fire this timer whenever a cycle completes
# The task itself will manage counting how many times it fires # The task itself will manage counting how many times it fires
if my_task.internal_data.get('event_fired'):
# If we manually send this event, this will be set
return True
repeat, delta = my_task.workflow.script_engine.evaluate(my_task, self.cycle_definition) repeat, delta = my_task.workflow.script_engine.evaluate(my_task, self.cycle_definition)
# This is the first time we've entered this event # This is the first time we've entered this event
@ -393,6 +406,9 @@ class CycleTimerEventDefinition(EventDefinition):
my_task.internal_data['start_time'] = None my_task.internal_data['start_time'] = None
super(CycleTimerEventDefinition, self).reset(my_task) super(CycleTimerEventDefinition, self).reset(my_task)
def __eq__(self, other):
return self.__class__.__name__ == other.__class__.__name__ and self.label == other.label
def serialize(self): def serialize(self):
retdict = super(CycleTimerEventDefinition, self).serialize() retdict = super(CycleTimerEventDefinition, self).serialize()
retdict['label'] = self.label retdict['label'] = self.label
@ -411,19 +427,27 @@ class MultipleEventDefinition(EventDefinition):
def event_type(self): def event_type(self):
return 'Multiple' return 'Multiple'
def catch(self, my_task, event_definition=None): def has_fired(self, my_task):
event_definition.catch(my_task, event_definition)
seen_events = my_task.internal_data.get('seen_events', [])
for event in self.event_definitions:
if isinstance(event, (TimerEventDefinition, CycleTimerEventDefinition)):
child = [c for c in my_task.children if c.task_spec.event_definition == event]
child[0].task_spec._update_hook(child[0])
child[0]._set_state(TaskState.MAYBE)
if event.has_fired(my_task):
seen_events.append(event)
if self.parallel: if self.parallel:
# Parallel multiple need to match all events # Parallel multiple need to match all events
return all(event in seen_events for event in self.event_definitions)
else:
return len(seen_events) > 0
def catch(self, my_task, event_definition=None):
event_definition.catch(my_task, event_definition)
seen_events = my_task.internal_data.get('seen_events', []) + [event_definition] seen_events = my_task.internal_data.get('seen_events', []) + [event_definition]
my_task._set_internal_data(seen_events=seen_events) my_task._set_internal_data(seen_events=seen_events)
if all(event in seen_events for event in self.event_definitions):
my_task._set_internal_data(event_fired=True)
else:
my_task._set_internal_data(event_fired=False)
else:
# Otherwise, matching one is sufficient
my_task._set_internal_data(event_fired=True)
def reset(self, my_task): def reset(self, my_task):
my_task.internal_data.pop('seen_events', None) my_task.internal_data.pop('seen_events', None)

View File

@ -54,7 +54,7 @@ class CatchingEvent(Simple, BpmnSpecMixin):
my_task._ready() my_task._ready()
super(CatchingEvent, self)._update_hook(my_task) super(CatchingEvent, self)._update_hook(my_task)
def _on_ready(self, my_task): def _on_ready_hook(self, my_task):
# None events don't propogate, so as soon as we're ready, we fire our event # None events don't propogate, so as soon as we're ready, we fire our event
if isinstance(self.event_definition, NoneEventDefinition): if isinstance(self.event_definition, NoneEventDefinition):
@ -63,7 +63,7 @@ class CatchingEvent(Simple, BpmnSpecMixin):
# If we have not seen the event we're waiting for, enter the waiting state # If we have not seen the event we're waiting for, enter the waiting state
if not self.event_definition.has_fired(my_task): if not self.event_definition.has_fired(my_task):
my_task._set_state(TaskState.WAITING) my_task._set_state(TaskState.WAITING)
super(CatchingEvent, self)._on_ready(my_task) super(CatchingEvent, self)._on_ready_hook(my_task)
def _on_complete_hook(self, my_task): def _on_complete_hook(self, my_task):

View File

@ -3,7 +3,7 @@ from functools import partial
from SpiffWorkflow.bpmn.serializer.bpmn_converters import BpmnTaskSpecConverter from SpiffWorkflow.bpmn.serializer.bpmn_converters import BpmnTaskSpecConverter
from SpiffWorkflow.bpmn.specs.events.StartEvent import StartEvent from SpiffWorkflow.bpmn.specs.events.StartEvent import StartEvent
from SpiffWorkflow.bpmn.specs.events.EndEvent import EndEvent from SpiffWorkflow.bpmn.specs.events.EndEvent import EndEvent
from SpiffWorkflow.bpmn.specs.events.IntermediateEvent import IntermediateThrowEvent, IntermediateCatchEvent, BoundaryEvent from SpiffWorkflow.bpmn.specs.events.IntermediateEvent import IntermediateThrowEvent, IntermediateCatchEvent, BoundaryEvent, EventBasedGateway
from SpiffWorkflow.spiff.specs.none_task import NoneTask from SpiffWorkflow.spiff.specs.none_task import NoneTask
from SpiffWorkflow.spiff.specs.manual_task import ManualTask from SpiffWorkflow.spiff.specs.manual_task import ManualTask
from SpiffWorkflow.spiff.specs.user_task import UserTask from SpiffWorkflow.spiff.specs.user_task import UserTask
@ -164,3 +164,7 @@ class ReceiveTaskConverter(SpiffEventConverter):
dct['prescript'] = spec.prescript dct['prescript'] = spec.prescript
dct['postscript'] = spec.postscript dct['postscript'] = spec.postscript
return dct return dct
class EventBasedGatewayConverter(SpiffEventConverter):
def __init__(self, data_converter=None, typename=None):
super().__init__(EventBasedGateway, data_converter, typename)

View File

@ -36,6 +36,20 @@ class EventBsedGatewayTest(BpmnWorkflowTestCase):
self.assertEqual(self.workflow.get_tasks_from_spec_name('message_2_event')[0].state, TaskState.CANCELLED) self.assertEqual(self.workflow.get_tasks_from_spec_name('message_2_event')[0].state, TaskState.CANCELLED)
self.assertEqual(self.workflow.get_tasks_from_spec_name('timer_event')[0].state, TaskState.CANCELLED) self.assertEqual(self.workflow.get_tasks_from_spec_name('timer_event')[0].state, TaskState.CANCELLED)
def testTimeout(self):
self.workflow.do_engine_steps()
waiting_tasks = self.workflow.get_waiting_tasks()
self.assertEqual(len(waiting_tasks), 1)
timer_event = waiting_tasks[0].task_spec.event_definition.event_definitions[-1]
self.workflow.catch(timer_event)
self.workflow.refresh_waiting_tasks()
self.workflow.do_engine_steps()
self.assertEqual(self.workflow.is_completed(), True)
self.assertEqual(self.workflow.get_tasks_from_spec_name('message_1_event')[0].state, TaskState.CANCELLED)
self.assertEqual(self.workflow.get_tasks_from_spec_name('message_2_event')[0].state, TaskState.CANCELLED)
self.assertEqual(self.workflow.get_tasks_from_spec_name('timer_event')[0].state, TaskState.COMPLETED)
def testMultipleStart(self): def testMultipleStart(self):
spec, subprocess = self.load_workflow_spec('multiple-start-parallel.bpmn', 'main') spec, subprocess = self.load_workflow_spec('multiple-start-parallel.bpmn', 'main')
workflow = BpmnWorkflow(spec) workflow = BpmnWorkflow(spec)

11
bin/pre Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
function error_handler() {
>&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
exit "$2"
}
trap 'error_handler ${LINENO} $?' ERR
set -o errtrace -o errexit -o nounset -o pipefail
script_dir="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
"${script_dir}/run_pyl" pre

View File

@ -16,6 +16,17 @@ react_projects=(
spiffworkflow-frontend spiffworkflow-frontend
) )
subcommand="${1:-}"
if [[ "$subcommand" == "pre" ]]; then
if [[ -n "$(git status --porcelain SpiffWorkflow)" ]]; then
echo "SpiffWorkflow has uncommitted changes. Running its test suite."
pushd SpiffWorkflow
make tests-par # run tests in parallel
popd
fi
fi
function get_python_dirs() { function get_python_dirs() {
(git ls-tree -r HEAD --name-only | grep -E '\.py$' | awk -F '/' '{print $1}' | sort | uniq | grep -v '\.' | grep -Ev '^(bin|migrations)$') || echo '' (git ls-tree -r HEAD --name-only | grep -E '\.py$' | awk -F '/' '{print $1}' | sort | uniq | grep -v '\.' | grep -Ev '^(bin|migrations)$') || echo ''
} }
@ -50,23 +61,34 @@ function run_pre_commmit() {
} }
for react_project in "${react_projects[@]}" ; do for react_project in "${react_projects[@]}" ; do
# if pre, only do stuff when there are changes
if [[ "$subcommand" != "pre" ]] || [[ -n "$(git status --porcelain "$react_project")" ]]; then
pushd "$react_project" pushd "$react_project"
npm run lint:fix npm run lint:fix
popd popd
fi
done done
for python_project in "${python_projects[@]}" ; do for python_project in "${python_projects[@]}" ; do
if [[ "$subcommand" != "pre" ]] || [[ -n "$(git status --porcelain "$python_project")" ]]; then
pushd "$python_project" pushd "$python_project"
run_fix_docstrings || run_fix_docstrings run_fix_docstrings || run_fix_docstrings
run_autoflake || run_autoflake run_autoflake || run_autoflake
popd popd
fi
done done
if [[ "$subcommand" != "pre" ]] || [[ -n "$(git status --porcelain "spiffworkflow-backend")" ]]; then
# rune_pre_commit only applies to spiffworkflow-backend at the moment
run_pre_commmit || run_pre_commmit run_pre_commmit || run_pre_commmit
fi
for python_project in "${python_projects[@]}"; do for python_project in "${python_projects[@]}"; do
if [[ "$subcommand" != "pre" ]] || [[ -n "$(git status --porcelain "$python_project")" ]]; then
pushd "$python_project" pushd "$python_project"
poetry install poetry install
poetry run mypy $(get_python_dirs) poetry run mypy $(get_python_dirs)
poetry run coverage run --parallel -m pytest poetry run coverage run --parallel -m pytest
popd popd
fi
done done

View File

@ -170,11 +170,13 @@ def set_user_sentry_context() -> None:
def handle_exception(exception: Exception) -> flask.wrappers.Response: def handle_exception(exception: Exception) -> flask.wrappers.Response:
"""Handles unexpected exceptions.""" """Handles unexpected exceptions."""
set_user_sentry_context() set_user_sentry_context()
sentry_link = None
if not isinstance(exception, ApiError) or exception.error_code != "invalid_token":
id = capture_exception(exception) id = capture_exception(exception)
organization_slug = current_app.config.get("SENTRY_ORGANIZATION_SLUG") organization_slug = current_app.config.get("SENTRY_ORGANIZATION_SLUG")
project_slug = current_app.config.get("SENTRY_PROJECT_SLUG") project_slug = current_app.config.get("SENTRY_PROJECT_SLUG")
sentry_link = None
if organization_slug and project_slug: if organization_slug and project_slug:
sentry_link = ( sentry_link = (
f"https://sentry.io/{organization_slug}/{project_slug}/events/{id}" f"https://sentry.io/{organization_slug}/{project_slug}/events/{id}"

View File

@ -9,7 +9,7 @@ set -o errtrace -o errexit -o nounset -o pipefail
if [[ -z "${BPMN_SPEC_ABSOLUTE_DIR:-}" ]]; then if [[ -z "${BPMN_SPEC_ABSOLUTE_DIR:-}" ]]; then
script_dir="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" script_dir="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
export BPMN_SPEC_ABSOLUTE_DIR="$script_dir/../../sample-process-models" export BPMN_SPEC_ABSOLUTE_DIR="$script_dir/../../../sample-process-models"
fi fi
if [[ -z "${SPIFFWORKFLOW_BACKEND_DOCKER_COMPOSE_PROFILE:-}" ]]; then if [[ -z "${SPIFFWORKFLOW_BACKEND_DOCKER_COMPOSE_PROFILE:-}" ]]; then

View File

@ -11,14 +11,22 @@ set -o errtrace -o errexit -o nounset -o pipefail
bpmn_models_absolute_dir="$1" bpmn_models_absolute_dir="$1"
git_commit_message="$2" git_commit_message="$2"
git_commit_username="$3" git_branch="$3"
git_commit_email="$4" git_commit_username="$4"
git_commit_email="$5"
git_commit_password="$6"
if [[ -z "${2:-}" ]]; then if [[ -z "${6:-}" ]]; then
>&2 echo "usage: $(basename "$0") [bpmn_models_absolute_dir] [git_commit_message]" >&2 echo "usage: $(basename "$0") [bpmn_models_absolute_dir] [git_commit_message] [git_branch] [git_commit_username] [git_commit_email]"
exit 1 exit 1
fi fi
function failed_to_get_lock() {
>&2 echo "ERROR: Failed to get lock."
exit 1
}
function run() {
cd "$bpmn_models_absolute_dir" cd "$bpmn_models_absolute_dir"
git add . git add .
@ -26,11 +34,19 @@ git add .
if [ -z "$(git status --porcelain)" ]; then if [ -z "$(git status --porcelain)" ]; then
echo "No changes to commit" echo "No changes to commit"
else else
if [[ -n "$git_commit_username" ]]; then PAT="${git_commit_username}:${git_commit_password}"
AUTH=$(echo -n "$PAT" | openssl base64 | tr -d '\n')
git config --local user.name "$git_commit_username" git config --local user.name "$git_commit_username"
fi
if [[ -n "$git_commit_email" ]]; then
git config --local user.email "$git_commit_email" git config --local user.email "$git_commit_email"
fi git config --local http.extraHeader "Authorization: Basic $AUTH"
git commit -m "$git_commit_message" git commit -m "$git_commit_message"
git push --set-upstream origin "$git_branch"
git config --unset --local http.extraHeader
fi fi
}
exec {lock_fd}>/var/lock/mylockfile || failed_to_get_lock
flock --timeout 60 "$lock_fd" || failed_to_get_lock
run
flock -u "$lock_fd"

View File

@ -18,7 +18,19 @@ set -o errtrace -o errexit -o nounset -o pipefail
if ! docker network inspect spiffworkflow > /dev/null 2>&1; then if ! docker network inspect spiffworkflow > /dev/null 2>&1; then
docker network create spiffworkflow docker network create spiffworkflow
fi fi
docker rm keycloak 2>/dev/null || echo 'no keycloak container found, safe to start new container'
# https://stackoverflow.com/a/60579344/6090676
container_name="keycloak"
if [[ -n "$(docker ps -qa -f name=$container_name)" ]]; then
echo ":: Found container - $container_name"
if [[ -n "$(docker ps -q -f name=$container_name)" ]]; then
echo ":: Stopping running container - $container_name"
docker stop $container_name
fi
echo ":: Removing stopped container - $container_name"
docker rm $container_name
fi
docker run \ docker run \
-p 7002:8080 \ -p 7002:8080 \
-d \ -d \

View File

@ -9,7 +9,7 @@ from flask_bpmn.models.db import db
from flask_bpmn.models.db import SpiffworkflowBaseDBModel from flask_bpmn.models.db import SpiffworkflowBaseDBModel
from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from spiffworkflow_backend.models.active_task_user import ActiveTaskUserModel from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.process_instance_processor import ( from spiffworkflow_backend.services.process_instance_processor import (
@ -47,7 +47,7 @@ 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() db.session.query(HumanTaskUserModel).delete()
for model in SpiffworkflowBaseDBModel._all_subclasses(): for model in SpiffworkflowBaseDBModel._all_subclasses():
db.session.query(model).delete() db.session.query(model).delete()

View File

@ -68,7 +68,7 @@ services:
- "7000:7000" - "7000:7000"
network_mode: host network_mode: host
volumes: volumes:
- ${BPMN_SPEC_ABSOLUTE_DIR:-./../sample-process-models}:/app/process_models - ${BPMN_SPEC_ABSOLUTE_DIR:-../../sample-process-models}:/app/process_models
- ./log:/app/log - ./log:/app/log
healthcheck: healthcheck:
test: curl localhost:7000/v1.0/status --fail test: curl localhost:7000/v1.0/status --fail
@ -82,7 +82,7 @@ services:
profiles: profiles:
- debug - debug
volumes: volumes:
- ${BPMN_SPEC_ABSOLUTE_DIR:-./../sample-process-models}:/app/process_models - ${BPMN_SPEC_ABSOLUTE_DIR:-../../sample-process-models}:/app/process_models
- ./:/app - ./:/app
command: /app/bin/boot_in_docker_debug_mode command: /app/bin/boot_in_docker_debug_mode

View File

@ -1,8 +1,8 @@
"""empty message """empty message
Revision ID: 4d75421c0af0 Revision ID: b99a4cb94b5b
Revises: Revises:
Create Date: 2022-12-06 17:42:56.417673 Create Date: 2022-12-20 10:45:08.295317
""" """
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 = '4d75421c0af0' revision = 'b99a4cb94b5b'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -77,6 +77,8 @@ def upgrade():
sa.Column('service_id', sa.String(length=255), nullable=False), sa.Column('service_id', sa.String(length=255), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True), sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('email', sa.String(length=255), nullable=True), sa.Column('email', sa.String(length=255), nullable=True),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True),
sa.Column('created_at_in_seconds', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('service', 'service_id', name='service_key'), sa.UniqueConstraint('service', 'service_id', name='service_key'),
sa.UniqueConstraint('uid') sa.UniqueConstraint('uid')
@ -174,11 +176,12 @@ def upgrade():
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'group_id', name='user_group_assignment_unique') sa.UniqueConstraint('user_id', 'group_id', name='user_group_assignment_unique')
) )
op.create_table('active_task', op.create_table('human_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('actual_owner_id', sa.Integer(), nullable=True),
sa.Column('lane_assignment_id', sa.Integer(), nullable=True), sa.Column('lane_assignment_id', sa.Integer(), nullable=True),
sa.Column('completed_by_user_id', sa.Integer(), nullable=True),
sa.Column('actual_owner_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),
@ -189,12 +192,15 @@ def upgrade():
sa.Column('task_type', sa.String(length=50), nullable=True), sa.Column('task_type', sa.String(length=50), nullable=True),
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('completed', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['actual_owner_id'], ['user.id'], ), sa.ForeignKeyConstraint(['actual_owner_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['completed_by_user_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['lane_assignment_id'], ['group.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='human_task_unique')
) )
op.create_index(op.f('ix_human_task_completed'), 'human_task', ['completed'], unique=False)
op.create_table('message_correlation', op.create_table('message_correlation',
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),
@ -255,23 +261,20 @@ def upgrade():
sa.Column('spiff_step', sa.Integer(), nullable=False), sa.Column('spiff_step', sa.Integer(), nullable=False),
sa.Column('task_json', sa.JSON(), nullable=False), sa.Column('task_json', sa.JSON(), nullable=False),
sa.Column('timestamp', sa.DECIMAL(precision=17, scale=6), nullable=False), sa.Column('timestamp', sa.DECIMAL(precision=17, scale=6), nullable=False),
sa.Column('completed_by_user_id', sa.Integer(), nullable=True),
sa.Column('lane_assignment_id', sa.Integer(), nullable=True),
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')
) )
op.create_table('active_task_user', op.create_table('human_task_user',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('active_task_id', sa.Integer(), nullable=False), sa.Column('human_task_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['active_task_id'], ['active_task.id'], ), sa.ForeignKeyConstraint(['human_task_id'], ['human_task.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('active_task_id', 'user_id', name='active_task_user_unique') sa.UniqueConstraint('human_task_id', 'user_id', name='human_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_human_task_user_human_task_id'), 'human_task_user', ['human_task_id'], unique=False)
op.create_index(op.f('ix_active_task_user_user_id'), 'active_task_user', ['user_id'], unique=False) op.create_index(op.f('ix_human_task_user_user_id'), 'human_task_user', ['user_id'], unique=False)
op.create_table('message_correlation_message_instance', op.create_table('message_correlation_message_instance',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('message_instance_id', sa.Integer(), nullable=False), sa.Column('message_instance_id', sa.Integer(), nullable=False),
@ -291,9 +294,9 @@ def downgrade():
op.drop_index(op.f('ix_message_correlation_message_instance_message_instance_id'), table_name='message_correlation_message_instance') op.drop_index(op.f('ix_message_correlation_message_instance_message_instance_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_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_index(op.f('ix_active_task_user_user_id'), table_name='active_task_user') op.drop_index(op.f('ix_human_task_user_user_id'), table_name='human_task_user')
op.drop_index(op.f('ix_active_task_user_active_task_id'), table_name='active_task_user') op.drop_index(op.f('ix_human_task_user_human_task_id'), table_name='human_task_user')
op.drop_table('active_task_user') op.drop_table('human_task_user')
op.drop_table('spiff_step_details') op.drop_table('spiff_step_details')
op.drop_index(op.f('ix_process_instance_metadata_key'), table_name='process_instance_metadata') op.drop_index(op.f('ix_process_instance_metadata_key'), table_name='process_instance_metadata')
op.drop_table('process_instance_metadata') op.drop_table('process_instance_metadata')
@ -304,7 +307,8 @@ def downgrade():
op.drop_index(op.f('ix_message_correlation_name'), table_name='message_correlation') op.drop_index(op.f('ix_message_correlation_name'), table_name='message_correlation')
op.drop_index(op.f('ix_message_correlation_message_correlation_property_id'), table_name='message_correlation') op.drop_index(op.f('ix_message_correlation_message_correlation_property_id'), table_name='message_correlation')
op.drop_table('message_correlation') op.drop_table('message_correlation')
op.drop_table('active_task') op.drop_index(op.f('ix_human_task_completed'), table_name='human_task')
op.drop_table('human_task')
op.drop_table('user_group_assignment') op.drop_table('user_group_assignment')
op.drop_table('secret') op.drop_table('secret')
op.drop_table('refresh_token') op.drop_table('refresh_token')

View File

@ -654,7 +654,7 @@ werkzeug = "*"
type = "git" type = "git"
url = "https://github.com/sartography/flask-bpmn" url = "https://github.com/sartography/flask-bpmn"
reference = "main" reference = "main"
resolved_reference = "860f2387bebdaa9220e9fbf6f8fa7f74e805d0d4" resolved_reference = "0f2d249d0e799bec912d46132e9ef9754fdacbd7"
[[package]] [[package]]
name = "Flask-Cors" name = "Flask-Cors"
@ -1851,7 +1851,7 @@ lxml = "*"
type = "git" type = "git"
url = "https://github.com/sartography/SpiffWorkflow" url = "https://github.com/sartography/SpiffWorkflow"
reference = "main" reference = "main"
resolved_reference = "ffb1686757f944065580dd2db8def73d6c1f0134" resolved_reference = "841bd63017bb1d92858456393f144b4e5b23c994"
[[package]] [[package]]
name = "SQLAlchemy" name = "SQLAlchemy"
@ -2563,7 +2563,6 @@ greenlet = [
{file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b0ff9878333823226d270417f24f4d06f235cb3e54d1103b71ea537a6a86ce"}, {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b0ff9878333823226d270417f24f4d06f235cb3e54d1103b71ea537a6a86ce"},
{file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000"}, {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000"},
{file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2"}, {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2"},
{file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0459d94f73265744fee4c2d5ec44c6f34aa8a31017e6e9de770f7bcf29710be9"},
{file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1"}, {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1"},
{file = "greenlet-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"}, {file = "greenlet-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"},
{file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"}, {file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"},
@ -2572,7 +2571,6 @@ greenlet = [
{file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:659f167f419a4609bc0516fb18ea69ed39dbb25594934bd2dd4d0401660e8a1e"}, {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:659f167f419a4609bc0516fb18ea69ed39dbb25594934bd2dd4d0401660e8a1e"},
{file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48"}, {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48"},
{file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764"}, {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764"},
{file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d38ffd0e81ba8ef347d2be0772e899c289b59ff150ebbbbe05dc61b1246eb4e0"},
{file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9"}, {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9"},
{file = "greenlet-2.0.1-cp38-cp38-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"}, {file = "greenlet-2.0.1-cp38-cp38-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"},
{file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"}, {file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"},
@ -2581,7 +2579,6 @@ greenlet = [
{file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:505138d4fa69462447a562a7c2ef723c6025ba12ac04478bc1ce2fcc279a2db5"}, {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:505138d4fa69462447a562a7c2ef723c6025ba12ac04478bc1ce2fcc279a2db5"},
{file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7"}, {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7"},
{file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d"}, {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d"},
{file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:662e8f7cad915ba75d8017b3e601afc01ef20deeeabf281bd00369de196d7726"},
{file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e"}, {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e"},
{file = "greenlet-2.0.1-cp39-cp39-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"}, {file = "greenlet-2.0.1-cp39-cp39-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"},
{file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"}, {file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"},
@ -2880,7 +2877,10 @@ orjson = [
{file = "orjson-3.8.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b68a42a31f8429728183c21fb440c21de1b62e5378d0d73f280e2d894ef8942e"}, {file = "orjson-3.8.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b68a42a31f8429728183c21fb440c21de1b62e5378d0d73f280e2d894ef8942e"},
{file = "orjson-3.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ff13410ddbdda5d4197a4a4c09969cb78c722a67550f0a63c02c07aadc624833"}, {file = "orjson-3.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ff13410ddbdda5d4197a4a4c09969cb78c722a67550f0a63c02c07aadc624833"},
{file = "orjson-3.8.0-cp310-none-win_amd64.whl", hash = "sha256:2d81e6e56bbea44be0222fb53f7b255b4e7426290516771592738ca01dbd053b"}, {file = "orjson-3.8.0-cp310-none-win_amd64.whl", hash = "sha256:2d81e6e56bbea44be0222fb53f7b255b4e7426290516771592738ca01dbd053b"},
{file = "orjson-3.8.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:200eae21c33f1f8b02a11f5d88d76950cd6fd986d88f1afe497a8ae2627c49aa"},
{file = "orjson-3.8.0-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:9529990f3eab54b976d327360aa1ff244a4b12cb5e4c5b3712fcdd96e8fe56d4"},
{file = "orjson-3.8.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e2defd9527651ad39ec20ae03c812adf47ef7662bdd6bc07dabb10888d70dc62"}, {file = "orjson-3.8.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e2defd9527651ad39ec20ae03c812adf47ef7662bdd6bc07dabb10888d70dc62"},
{file = "orjson-3.8.0-cp311-none-win_amd64.whl", hash = "sha256:b21c7af0ff6228ca7105f54f0800636eb49201133e15ddb80ac20c1ce973ef07"},
{file = "orjson-3.8.0-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9e6ac22cec72d5b39035b566e4b86c74b84866f12b5b0b6541506a080fb67d6d"}, {file = "orjson-3.8.0-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9e6ac22cec72d5b39035b566e4b86c74b84866f12b5b0b6541506a080fb67d6d"},
{file = "orjson-3.8.0-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e2f4a5542f50e3d336a18cb224fc757245ca66b1fd0b70b5dd4471b8ff5f2b0e"}, {file = "orjson-3.8.0-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e2f4a5542f50e3d336a18cb224fc757245ca66b1fd0b70b5dd4471b8ff5f2b0e"},
{file = "orjson-3.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1418feeb8b698b9224b1f024555895169d481604d5d884498c1838d7412794c"}, {file = "orjson-3.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1418feeb8b698b9224b1f024555895169d481604d5d884498c1838d7412794c"},
@ -2989,18 +2989,7 @@ psycopg2 = [
{file = "psycopg2-2.9.4.tar.gz", hash = "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f"}, {file = "psycopg2-2.9.4.tar.gz", hash = "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f"},
] ]
pyasn1 = [ pyasn1 = [
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
] ]
pycodestyle = [ pycodestyle = [

View File

@ -509,6 +509,119 @@ paths:
schema: schema:
$ref: "#/components/schemas/OkTrue" $ref: "#/components/schemas/OkTrue"
/process-instances/for-me:
parameters:
- name: process_model_identifier
in: query
required: false
description: The unique id of an existing process model.
schema:
type: string
- name: page
in: query
required: false
description: The page number to return. Defaults to page 1.
schema:
type: integer
- name: per_page
in: query
required: false
description: The page number to return. Defaults to page 1.
schema:
type: integer
- name: start_from
in: query
required: false
description: For filtering - beginning of start window - in seconds since epoch
schema:
type: integer
- name: start_to
in: query
required: false
description: For filtering - end of start window - in seconds since epoch
schema:
type: integer
- name: end_from
in: query
required: false
description: For filtering - beginning of end window - in seconds since epoch
schema:
type: integer
- name: end_to
in: query
required: false
description: For filtering - end of end window - in seconds since epoch
schema:
type: integer
- name: process_status
in: query
required: false
description: For filtering - not_started, user_input_required, waiting, complete, error, or suspended
schema:
type: string
- name: initiated_by_me
in: query
required: false
description: For filtering - show instances initiated by me
schema:
type: boolean
- name: with_tasks_completed_by_me
in: query
required: false
description: For filtering - show instances with tasks completed by me
schema:
type: boolean
- name: with_tasks_completed_by_my_group
in: query
required: false
description: For filtering - show instances with tasks completed by my group
schema:
type: boolean
- name: with_relation_to_me
in: query
required: false
description: For filtering - show instances that have something to do with me
schema:
type: boolean
- name: user_filter
in: query
required: false
description: For filtering - indicates the user has manually entered a query
schema:
type: boolean
- name: report_identifier
in: query
required: false
description: Specifies the identifier of a report to use, if any
schema:
type: string
- name: report_id
in: query
required: false
description: Specifies the identifier of a report to use, if any
schema:
type: integer
- name: user_group_identifier
in: query
required: false
description: The identifier of the group to get the process instances for
schema:
type: string
get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_list_for_me
summary: Returns a list of process instances that are associated with me.
tags:
- Process Instances
responses:
"200":
description: Workflow.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Workflow"
/process-instances: /process-instances:
parameters: parameters:
- name: process_model_identifier - name: process_model_identifier
@ -577,6 +690,12 @@ paths:
description: For filtering - show instances with tasks completed by my group description: For filtering - show instances with tasks completed by my group
schema: schema:
type: boolean type: boolean
- name: with_relation_to_me
in: query
required: false
description: For filtering - show instances that have something to do with me
schema:
type: boolean
- name: user_filter - name: user_filter
in: query in: query
required: false required: false
@ -595,9 +714,15 @@ paths:
description: Specifies the identifier of a report to use, if any description: Specifies the identifier of a report to use, if any
schema: schema:
type: integer type: integer
- name: user_group_identifier
in: query
required: false
description: The identifier of the group to get the process instances for
schema:
type: string
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_list operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_list
summary: Returns a list of process instances for a given process model summary: Returns a list of process instances.
tags: tags:
- Process Instances - Process Instances
responses: responses:
@ -610,15 +735,9 @@ paths:
items: items:
$ref: "#/components/schemas/Workflow" $ref: "#/components/schemas/Workflow"
/process-models/{process_group_id}/{process_model_id}/script-unit-tests: /process-models/{modified_process_model_identifier}/script-unit-tests:
parameters: parameters:
- name: process_group_id - name: modified_process_model_identifier
in: path
required: true
description: The unique id of an existing process group
schema:
type: string
- name: process_model_id
in: path in: path
required: true required: true
description: The unique id of an existing process model. description: The unique id of an existing process model.
@ -637,15 +756,9 @@ paths:
schema: schema:
$ref: "#/components/schemas/Workflow" $ref: "#/components/schemas/Workflow"
/process-models/{process_group_id}/{process_model_id}/script-unit-tests/run: /process-models/{modified_process_model_identifier}/script-unit-tests/run:
parameters: parameters:
- name: process_group_id - name: modified_process_model_identifier
in: path
required: true
description: The unique id of an existing process group
schema:
type: string
- name: process_model_id
in: path in: path
required: true required: true
description: The unique id of an existing process model. description: The unique id of an existing process model.
@ -685,6 +798,133 @@ paths:
schema: schema:
$ref: "#/components/schemas/Workflow" $ref: "#/components/schemas/Workflow"
/process-instances/for-me/{modified_process_model_identifier}/{process_instance_id}/task-info:
parameters:
- name: modified_process_model_identifier
in: path
required: true
description: The unique id of an existing process model
schema:
type: string
- name: process_instance_id
in: path
required: true
description: The unique id of an existing process instance.
schema:
type: integer
- name: process_identifier
in: query
required: false
description: The identifier of the process to use for the diagram. Useful for displaying the diagram for a call activity.
schema:
type: string
- name: all_tasks
in: query
required: false
description: If true, this wil return all tasks associated with the process instance and not just user tasks.
schema:
type: boolean
- name: spiff_step
in: query
required: false
description: If set will return the tasks as they were during a specific step of execution.
schema:
type: integer
get:
tags:
- Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_task_list_without_task_data_for_me
summary: returns the list of all user tasks associated with process instance without the task data
responses:
"200":
description: list of tasks
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Task"
/process-instances/{modified_process_model_identifier}/{process_instance_id}/task-info:
parameters:
- name: modified_process_model_identifier
in: path
required: true
description: The unique id of an existing process model
schema:
type: string
- name: process_instance_id
in: path
required: true
description: The unique id of an existing process instance.
schema:
type: integer
- name: process_identifier
in: query
required: false
description: The identifier of the process to use for the diagram. Useful for displaying the diagram for a call activity.
schema:
type: string
- name: all_tasks
in: query
required: false
description: If true, this wil return all tasks associated with the process instance and not just user tasks.
schema:
type: boolean
- name: spiff_step
in: query
required: false
description: If set will return the tasks as they were during a specific step of execution.
schema:
type: integer
get:
tags:
- Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_task_list_without_task_data
summary: returns the list of all user tasks associated with process instance without the task data
responses:
"200":
description: list of tasks
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Task"
/process-instances/for-me/{modified_process_model_identifier}/{process_instance_id}:
parameters:
- name: modified_process_model_identifier
in: path
required: true
description: The unique id of an existing process model
schema:
type: string
- name: process_instance_id
in: path
required: true
description: The unique id of an existing process instance.
schema:
type: integer
- name: process_identifier
in: query
required: false
description: The identifier of the process to use for the diagram. Useful for displaying the diagram for a call activity.
schema:
type: string
get:
tags:
- Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_show_for_me
summary: Show information about a process instance that is associated with me
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
@ -757,7 +997,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/Workflow" $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: parameters:
- name: process_instance_id - name: process_instance_id
in: path in: path
@ -778,7 +1018,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/OkTrue" $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: parameters:
- name: process_instance_id - name: process_instance_id
in: path in: path
@ -799,7 +1039,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/OkTrue" $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: parameters:
- name: process_instance_id - name: process_instance_id
in: path in: path
@ -1088,6 +1328,12 @@ paths:
/tasks/for-my-groups: /tasks/for-my-groups:
parameters: parameters:
- name: user_group_identifier
in: query
required: false
description: The identifier of the group to get the tasks for
schema:
type: string
- name: page - name: page
in: query in: query
required: false required: false
@ -1115,6 +1361,22 @@ paths:
items: items:
$ref: "#/components/schemas/Task" $ref: "#/components/schemas/Task"
/user-groups/for-current-user:
get:
tags:
- Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.user_group_list_for_current_user
summary: Group identifiers for current logged in user
responses:
"200":
description: list of user groups
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Task"
/task-data/{modified_process_model_identifier}/{process_instance_id}: /task-data/{modified_process_model_identifier}/{process_instance_id}:
parameters: parameters:
- name: modified_process_model_identifier - name: modified_process_model_identifier
@ -1144,8 +1406,8 @@ paths:
get: get:
tags: tags:
- Process Instances - Process Instances
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_task_list operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_task_list_with_task_data
summary: returns the list of all user tasks associated with process instance summary: returns the list of all user tasks associated with process instance with the task data
responses: responses:
"200": "200":
description: list of tasks description: list of tasks

View File

@ -17,5 +17,3 @@ GIT_CLONE_URL_FOR_PUBLISHING = environ.get(
) )
GIT_USERNAME = "sartography-automated-committer" GIT_USERNAME = "sartography-automated-committer"
GIT_USER_EMAIL = f"{GIT_USERNAME}@users.noreply.github.com" GIT_USER_EMAIL = f"{GIT_USERNAME}@users.noreply.github.com"
GIT_BRANCH_TO_PUBLISH_TO = "main"
GIT_BRANCH = "main"

View File

@ -9,5 +9,5 @@ permissions:
admin: admin:
groups: [admin, common-user] groups: [admin, common-user]
users: [] users: []
allowed_permissions: [create, read, update, delete, list, instantiate] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*

View File

@ -17,11 +17,9 @@ groups:
dan, dan,
mike, mike,
jason, jason,
j,
jarrad, jarrad,
elizabeth, elizabeth,
jon, jon,
natalia,
] ]
Finance Team: Finance Team:
@ -32,12 +30,10 @@ groups:
dan, dan,
mike, mike,
jason, jason,
j,
amir, amir,
jarrad, jarrad,
elizabeth, elizabeth,
jon, jon,
natalia,
sasha, sasha,
fin, fin,
fin1, fin1,
@ -50,6 +46,7 @@ groups:
fin, fin,
fin1, fin1,
harmeet, harmeet,
jason,
sasha, sasha,
manuchehr, manuchehr,
lead, lead,
@ -63,6 +60,15 @@ groups:
harmeet, harmeet,
] ]
admin-ro:
users:
[
j,
]
test:
users: [natalia]
permissions: permissions:
admin: admin:
groups: [admin] groups: [admin]
@ -70,6 +76,17 @@ permissions:
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*
admin-readonly:
groups: [admin-ro]
users: []
allowed_permissions: [read]
uri: /*
admin-process-instances-for-readonly:
groups: [admin-ro]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/*
tasks-crud: tasks-crud:
groups: [everybody] groups: [everybody]
users: [] users: []
@ -80,6 +97,11 @@ permissions:
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/service-tasks uri: /v1.0/service-tasks
user-groups-for-current-user:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/user-groups/for-current-user
# read all for everybody # read all for everybody
@ -93,15 +115,15 @@ permissions:
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-models/* uri: /v1.0/process-models/*
read-all-process-instance: read-all-process-instances-for-me:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-instances/* uri: /v1.0/process-instances/for-me/*
read-process-instance-reports: read-process-instance-reports:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/reports/* uri: /v1.0/process-instances/reports/*
processes-read: processes-read:
groups: [everybody] groups: [everybody]
@ -109,12 +131,6 @@ permissions:
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/processes uri: /v1.0/processes
task-data-read:
groups: [demo]
users: []
allowed_permissions: [read]
uri: /v1.0/task-data/*
manage-procurement-admin: manage-procurement-admin:
groups: ["Project Lead"] groups: ["Project Lead"]
@ -153,44 +169,30 @@ permissions:
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement:procurement:* uri: /v1.0/process-groups/manage-procurement:procurement:*
manage-revenue-streams-instantiate:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-models/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-revenue-streams-instances: manage-revenue-streams-instances:
groups: ["core-contributor", "demo"] groups: ["core-contributor", "demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/* uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instantiate:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-models/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-invoice-instances: manage-procurement-invoice-instances:
groups: ["core-contributor", "demo"] groups: ["core-contributor", "demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:* uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instantiate:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-models/manage-procurement:vendor-lifecycle-management:*
manage-procurement-instances: manage-procurement-instances:
groups: ["core-contributor", "demo"] groups: ["core-contributor", "demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:* uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:*
core1-admin-models-instantiate: create-test-instances:
groups: ["core-contributor", "Finance Team"] groups: ["test"]
users: [] users: []
allowed_permissions: [create] allowed_permissions: [create, read]
uri: /v1.0/process-models/misc:category_number_one:process-model-with-form/process-instances uri: /v1.0/process-instances/misc:test:*
core1-admin-instances: core1-admin-instances:
groups: ["core-contributor", "Finance Team"] groups: ["core-contributor", "Finance Team"]
users: [] users: []

View File

@ -0,0 +1,170 @@
default_group: everybody
groups:
admin:
users:
[
admin,
jakub,
kb,
alex,
dan,
mike,
jason,
j,
jarrad,
elizabeth,
jon,
natalia,
]
Finance Team:
users:
[
jakub,
alex,
dan,
mike,
jason,
j,
amir,
jarrad,
elizabeth,
jon,
natalia,
sasha,
fin,
fin1,
]
demo:
users:
[
core,
fin,
fin1,
harmeet,
sasha,
manuchehr,
lead,
lead1
]
core-contributor:
users:
[
core,
harmeet,
]
permissions:
admin:
groups: [admin]
users: []
allowed_permissions: [read]
uri: /*
admin-process-instances:
groups: [admin]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/*
tasks-crud:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/service-tasks
user-groups-for-current-user:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/user-groups/for-current-user
# read all for everybody
read-all-process-groups:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-groups/*
read-all-process-models:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-models/*
read-all-process-instances-for-me:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-instances/for-me/*
manage-process-instance-reports:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/reports/*
processes-read:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/processes
manage-procurement-admin-instances:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement:*
manage-procurement-admin-instances-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement/*
manage-procurement-admin-instance-logs:
groups: ["Project Lead"]
users: []
allowed_permissions: [read]
uri: /v1.0/logs/manage-procurement:*
manage-procurement-admin-instance-logs-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [read]
uri: /v1.0/logs/manage-procurement/*
manage-revenue-streams-instances:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-revenue-streams-instance-logs:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [read]
uri: /v1.0/logs/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-invoice-instance-logs:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [read]
uri: /v1.0/logs/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:*
manage-procurement-instance-logs:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [read]
uri: /v1.0/logs/manage-procurement:vendor-lifecycle-management:*

View File

@ -15,7 +15,6 @@ groups:
jarrad, jarrad,
elizabeth, elizabeth,
jon, jon,
natalia,
] ]
Finance Team: Finance Team:
@ -31,7 +30,6 @@ groups:
jarrad, jarrad,
elizabeth, elizabeth,
jon, jon,
natalia,
sasha, sasha,
fin, fin,
fin1, fin1,
@ -56,6 +54,8 @@ groups:
core, core,
harmeet, harmeet,
] ]
test:
users: [natalia]
permissions: permissions:
admin: admin:
@ -75,6 +75,11 @@ permissions:
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/service-tasks uri: /v1.0/service-tasks
user-groups-for-current-user:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/user-groups/for-current-user
# read all for everybody # read all for everybody
@ -88,15 +93,15 @@ permissions:
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-models/* uri: /v1.0/process-models/*
read-all-process-instance: read-all-process-instances-for-me:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-instances/* uri: /v1.0/process-instances/for-me/*
read-process-instance-reports: read-process-instance-reports:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/reports/* uri: /v1.0/process-instances/reports/*
processes-read: processes-read:
groups: [everybody] groups: [everybody]
@ -104,12 +109,6 @@ permissions:
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/processes uri: /v1.0/processes
task-data-read:
groups: [demo]
users: []
allowed_permissions: [read]
uri: /v1.0/task-data/*
manage-procurement-admin: manage-procurement-admin:
groups: ["Project Lead"] groups: ["Project Lead"]
@ -148,35 +147,26 @@ permissions:
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement:procurement:* uri: /v1.0/process-groups/manage-procurement:procurement:*
manage-revenue-streams-instantiate:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-models/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-revenue-streams-instances: manage-revenue-streams-instances:
groups: ["core-contributor", "demo"] groups: ["core-contributor", "demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/* uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instantiate:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-models/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-invoice-instances: manage-procurement-invoice-instances:
groups: ["core-contributor", "demo"] groups: ["core-contributor", "demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:* uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instantiate:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-models/manage-procurement:vendor-lifecycle-management:*
manage-procurement-instances: manage-procurement-instances:
groups: ["core-contributor", "demo"] groups: ["core-contributor", "demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:* uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:*
create-test-instances:
groups: ["test"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:test:*

View File

@ -4,3 +4,4 @@ from os import environ
GIT_BRANCH = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="staging") GIT_BRANCH = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="staging")
GIT_BRANCH_TO_PUBLISH_TO = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="main") GIT_BRANCH_TO_PUBLISH_TO = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="main")
GIT_COMMIT_ON_SAVE = False GIT_COMMIT_ON_SAVE = False
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = "staging.yml"

View File

@ -15,6 +15,7 @@ SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get(
SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get( SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get(
"SPIFFWORKFLOW_BACKEND_LOG_LEVEL", default="debug" "SPIFFWORKFLOW_BACKEND_LOG_LEVEL", default="debug"
) )
GIT_COMMIT_ON_SAVE = False
# NOTE: set this here since nox shoves tests and src code to # NOTE: set this here since nox shoves tests and src code to
# different places and this allows us to know exactly where we are at the start # different places and this allows us to know exactly where we are at the start

View File

@ -17,7 +17,7 @@ from spiffworkflow_backend.models.user_group_assignment import (
from spiffworkflow_backend.models.principal import PrincipalModel # noqa: F401 from spiffworkflow_backend.models.principal import PrincipalModel # noqa: F401
from spiffworkflow_backend.models.active_task import ActiveTaskModel # noqa: F401 from spiffworkflow_backend.models.human_task import HumanTaskModel # noqa: F401
from spiffworkflow_backend.models.spec_reference import ( from spiffworkflow_backend.models.spec_reference import (
SpecReferenceCache, SpecReferenceCache,
) # noqa: F401 ) # noqa: F401

View File

@ -1,4 +1,4 @@
"""Active_task.""" """Human_task."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
@ -8,7 +8,6 @@ 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 spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
@ -17,29 +16,30 @@ from spiffworkflow_backend.models.user import UserModel
if TYPE_CHECKING: if TYPE_CHECKING:
from spiffworkflow_backend.models.active_task_user import ( # noqa: F401 from spiffworkflow_backend.models.human_task_user import ( # noqa: F401
ActiveTaskUserModel, HumanTaskUserModel,
) )
@dataclass @dataclass
class ActiveTaskModel(SpiffworkflowBaseDBModel): class HumanTaskModel(SpiffworkflowBaseDBModel):
"""ActiveTaskModel.""" """HumanTaskModel."""
__tablename__ = "active_task" __tablename__ = "human_task"
__table_args__ = ( __table_args__ = (
db.UniqueConstraint( db.UniqueConstraint("task_id", "process_instance_id", name="human_task_unique"),
"task_id", "process_instance_id", name="active_task_unique"
),
) )
actual_owner: RelationshipProperty[UserModel] = relationship(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
) )
actual_owner_id: int = db.Column(ForeignKey(UserModel.id))
lane_assignment_id: int | None = db.Column(ForeignKey(GroupModel.id)) lane_assignment_id: int | None = db.Column(ForeignKey(GroupModel.id))
completed_by_user_id: int = db.Column(ForeignKey(UserModel.id), nullable=True)
actual_owner_id: int = db.Column(ForeignKey(UserModel.id))
# actual_owner: RelationshipProperty[UserModel] = relationship(UserModel)
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))
@ -52,17 +52,18 @@ class ActiveTaskModel(SpiffworkflowBaseDBModel):
task_type: str = db.Column(db.String(50)) task_type: str = db.Column(db.String(50))
task_status: str = db.Column(db.String(50)) task_status: str = db.Column(db.String(50))
process_model_display_name: str = db.Column(db.String(255)) process_model_display_name: str = db.Column(db.String(255))
completed: bool = db.Column(db.Boolean, default=False, nullable=False, index=True)
active_task_users = relationship("ActiveTaskUserModel", cascade="delete") human_task_users = relationship("HumanTaskUserModel", cascade="delete")
potential_owners = relationship( # type: ignore potential_owners = relationship( # type: ignore
"UserModel", "UserModel",
viewonly=True, viewonly=True,
secondary="active_task_user", secondary="human_task_user",
overlaps="active_task_user,users", overlaps="human_task_user,users",
) )
@classmethod @classmethod
def to_task(cls, task: ActiveTaskModel) -> Task: def to_task(cls, task: HumanTaskModel) -> Task:
"""To_task.""" """To_task."""
new_task = Task( new_task = Task(
task.task_id, task.task_id,
@ -79,7 +80,7 @@ class ActiveTaskModel(SpiffworkflowBaseDBModel):
if hasattr(task, "process_model_identifier"): if hasattr(task, "process_model_identifier"):
new_task.process_model_identifier = task.process_model_identifier new_task.process_model_identifier = task.process_model_identifier
# active tasks only have status when getting the list on the home page # human tasks only have status when getting the list on the home page
# and it comes from the process_instance. it should not be confused with task_status. # and it comes from the process_instance. it should not be confused with task_status.
if hasattr(task, "status"): if hasattr(task, "status"):
new_task.process_instance_status = task.status new_task.process_instance_status = task.status

View File

@ -1,4 +1,4 @@
"""Active_task_user.""" """Human_task_user."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
@ -7,26 +7,26 @@ 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 spiffworkflow_backend.models.active_task import ActiveTaskModel from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
@dataclass @dataclass
class ActiveTaskUserModel(SpiffworkflowBaseDBModel): class HumanTaskUserModel(SpiffworkflowBaseDBModel):
"""ActiveTaskUserModel.""" """HumanTaskUserModel."""
__tablename__ = "active_task_user" __tablename__ = "human_task_user"
__table_args__ = ( __table_args__ = (
db.UniqueConstraint( db.UniqueConstraint(
"active_task_id", "human_task_id",
"user_id", "user_id",
name="active_task_user_unique", name="human_task_user_unique",
), ),
) )
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
active_task_id = db.Column( human_task_id = db.Column(
ForeignKey(ActiveTaskModel.id), nullable=False, index=True # type: ignore ForeignKey(HumanTaskModel.id), nullable=False, index=True # type: ignore
) )
user_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True) user_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True)

View File

@ -26,34 +26,12 @@ class ProcessInstanceNotFoundError(Exception):
"""ProcessInstanceNotFoundError.""" """ProcessInstanceNotFoundError."""
class NavigationItemSchema(Schema): class ProcessInstanceTaskDataCannotBeUpdatedError(Exception):
"""NavigationItemSchema.""" """ProcessInstanceTaskDataCannotBeUpdatedError."""
class Meta:
"""Meta."""
fields = [ class ProcessInstanceCannotBeDeletedError(Exception):
"spec_id", """ProcessInstanceCannotBeDeletedError."""
"name",
"spec_type",
"task_id",
"description",
"backtracks",
"indent",
"lane",
"state",
"children",
]
unknown = INCLUDE
state = marshmallow.fields.String(required=False, allow_none=True)
description = marshmallow.fields.String(required=False, allow_none=True)
backtracks = marshmallow.fields.String(required=False, allow_none=True)
lane = marshmallow.fields.String(required=False, allow_none=True)
task_id = marshmallow.fields.String(required=False, allow_none=True)
children = marshmallow.fields.List(
marshmallow.fields.Nested(lambda: NavigationItemSchema())
)
class ProcessInstanceStatus(SpiffEnum): class ProcessInstanceStatus(SpiffEnum):
@ -82,7 +60,11 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
process_initiator_id: int = db.Column(ForeignKey(UserModel.id), nullable=False) process_initiator_id: int = db.Column(ForeignKey(UserModel.id), nullable=False)
process_initiator = relationship("UserModel") process_initiator = relationship("UserModel")
active_tasks = relationship("ActiveTaskModel", cascade="delete") # type: ignore human_tasks = relationship(
"HumanTaskModel",
cascade="delete",
primaryjoin="and_(HumanTaskModel.process_instance_id==ProcessInstanceModel.id, HumanTaskModel.completed == False)",
) # type: ignore
message_instances = relationship("MessageInstanceModel", cascade="delete") # type: ignore message_instances = relationship("MessageInstanceModel", cascade="delete") # type: ignore
message_correlations = relationship("MessageCorrelationModel", cascade="delete") # type: ignore message_correlations = relationship("MessageCorrelationModel", cascade="delete") # type: ignore
@ -131,6 +113,19 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
"""Validate_status.""" """Validate_status."""
return self.validate_enum_field(key, value, ProcessInstanceStatus) return self.validate_enum_field(key, value, ProcessInstanceStatus)
def can_submit_task(self) -> bool:
"""Can_submit_task."""
return not self.has_terminal_status() and self.status != "suspended"
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): class ProcessInstanceModelSchema(Schema):
"""ProcessInstanceModelSchema.""" """ProcessInstanceModelSchema."""

View File

@ -8,7 +8,7 @@ from flask_bpmn.models.db import SpiffworkflowBaseDBModel
@dataclass @dataclass
class SpiffLoggingModel(SpiffworkflowBaseDBModel): class SpiffLoggingModel(SpiffworkflowBaseDBModel):
"""LoggingModel.""" """SpiffLoggingModel."""
__tablename__ = "spiff_logging" __tablename__ = "spiff_logging"
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(db.Integer, primary_key=True)

View File

@ -1,13 +1,11 @@
"""Spiff_step_details.""" """Spiff_step_details."""
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional
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 deferred from sqlalchemy.orm import deferred
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
@ -20,10 +18,13 @@ class SpiffStepDetailsModel(SpiffworkflowBaseDBModel):
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
) )
# human_task_id: int = db.Column(
# ForeignKey(HumanTaskModel.id) # type: ignore
# )
spiff_step: int = db.Column(db.Integer, nullable=False) spiff_step: int = db.Column(db.Integer, nullable=False)
task_json: dict = deferred(db.Column(db.JSON, nullable=False)) # type: ignore task_json: dict = deferred(db.Column(db.JSON, nullable=False)) # type: ignore
timestamp: float = db.Column(db.DECIMAL(17, 6), nullable=False) timestamp: float = db.Column(db.DECIMAL(17, 6), nullable=False)
completed_by_user_id: int = db.Column(db.Integer, nullable=True) # completed_by_user_id: int = db.Column(db.Integer, nullable=True)
lane_assignment_id: Optional[int] = db.Column( # lane_assignment_id: Optional[int] = db.Column(
ForeignKey(GroupModel.id), nullable=True # ForeignKey(GroupModel.id), nullable=True
) # )

View File

@ -37,6 +37,8 @@ class UserModel(SpiffworkflowBaseDBModel):
service_id = db.Column(db.String(255), nullable=False, unique=False) service_id = db.Column(db.String(255), nullable=False, unique=False)
name = db.Column(db.String(255)) name = db.Column(db.String(255))
email = db.Column(db.String(255)) email = db.Column(db.String(255))
updated_at_in_seconds: int = db.Column(db.Integer)
created_at_in_seconds: int = db.Column(db.Integer)
user_group_assignments = relationship("UserGroupAssignmentModel", cascade="delete") # type: ignore user_group_assignments = relationship("UserGroupAssignmentModel", cascade="delete") # type: ignore
groups = relationship( # type: ignore groups = relationship( # type: ignore

View File

@ -16,8 +16,9 @@ from flask_bpmn.api.api_error import ApiError
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.authentication_service import AuthenticationService
from spiffworkflow_backend.services.authentication_service import ( from spiffworkflow_backend.services.authentication_service import (
AuthenticationService, MissingAccessTokenError,
) )
from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.user_service import UserService from spiffworkflow_backend.services.user_service import UserService
@ -268,10 +269,10 @@ def login_api_return(code: str, state: str, session_state: str) -> str:
code, "/v1.0/login_api_return" code, "/v1.0/login_api_return"
) )
access_token: str = auth_token_object["access_token"] access_token: str = auth_token_object["access_token"]
assert access_token # noqa: S101 if access_token is None:
raise MissingAccessTokenError("Cannot find the access token for the request")
return access_token return access_token
# return redirect("localhost:7000/v1.0/ui")
# return {'uid': 'user_1'}
def logout(id_token: str, redirect_url: Optional[str]) -> Response: def logout(id_token: str, redirect_url: Optional[str]) -> Response:

View File

@ -16,6 +16,10 @@ from werkzeug.wrappers import Response
from spiffworkflow_backend.models.refresh_token import RefreshTokenModel from spiffworkflow_backend.models.refresh_token import RefreshTokenModel
class MissingAccessTokenError(Exception):
"""MissingAccessTokenError."""
class AuthenticationProviderTypes(enum.Enum): class AuthenticationProviderTypes(enum.Enum):
"""AuthenticationServiceProviders.""" """AuthenticationServiceProviders."""

View File

@ -19,8 +19,8 @@ from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy import text from sqlalchemy import text
from spiffworkflow_backend.models.active_task import ActiveTaskModel
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel
from spiffworkflow_backend.models.permission_target import PermissionTargetModel from spiffworkflow_backend.models.permission_target import PermissionTargetModel
from spiffworkflow_backend.models.principal import MissingPrincipalError from spiffworkflow_backend.models.principal import MissingPrincipalError
@ -37,8 +37,8 @@ class PermissionsFileNotSetError(Exception):
"""PermissionsFileNotSetError.""" """PermissionsFileNotSetError."""
class ActiveTaskNotFoundError(Exception): class HumanTaskNotFoundError(Exception):
"""ActiveTaskNotFoundError.""" """HumanTaskNotFoundError."""
class UserDoesNotHaveAccessToTaskError(Exception): class UserDoesNotHaveAccessToTaskError(Exception):
@ -429,17 +429,17 @@ class AuthorizationService:
user: UserModel, user: UserModel,
) -> bool: ) -> bool:
"""Assert_user_can_complete_spiff_task.""" """Assert_user_can_complete_spiff_task."""
active_task = ActiveTaskModel.query.filter_by( human_task = HumanTaskModel.query.filter_by(
task_name=spiff_task.task_spec.name, task_name=spiff_task.task_spec.name,
process_instance_id=process_instance_id, process_instance_id=process_instance_id,
).first() ).first()
if active_task is None: if human_task is None:
raise ActiveTaskNotFoundError( raise HumanTaskNotFoundError(
f"Could find an active task with task name '{spiff_task.task_spec.name}'" f"Could find an human task with task name '{spiff_task.task_spec.name}'"
f" for process instance '{process_instance_id}'" f" for process instance '{process_instance_id}'"
) )
if user not in active_task.potential_owners: if user not in human_task.potential_owners:
raise UserDoesNotHaveAccessToTaskError( raise UserDoesNotHaveAccessToTaskError(
f"User {user.username} does not have access to update task'{spiff_task.task_spec.name}'" f"User {user.username} does not have access to update task'{spiff_task.task_spec.name}'"
f" for process instance '{process_instance_id}'" f" for process instance '{process_instance_id}'"
@ -485,7 +485,7 @@ class AuthorizationService:
cls.import_permissions_from_yaml_file() cls.import_permissions_from_yaml_file()
if is_new_user: if is_new_user:
UserService.add_user_to_active_tasks_if_appropriate(user_model) UserService.add_user_to_human_tasks_if_appropriate(user_model)
# this cannot be None so ignore mypy # this cannot be None so ignore mypy
return user_model # type: ignore return user_model # type: ignore

View File

@ -68,8 +68,17 @@ class GitService:
return cls.run_shell_command_to_get_stdout(shell_command) return cls.run_shell_command_to_get_stdout(shell_command)
@classmethod @classmethod
def commit(cls, message: str, repo_path: Optional[str] = None) -> str: def commit(
cls,
message: str,
repo_path: Optional[str] = None,
branch_name: Optional[str] = None,
) -> str:
"""Commit.""" """Commit."""
cls.check_for_basic_configs()
branch_name_to_use = branch_name
if branch_name_to_use is None:
branch_name_to_use = current_app.config["GIT_BRANCH"]
repo_path_to_use = repo_path repo_path_to_use = repo_path
if repo_path is None: if repo_path is None:
repo_path_to_use = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"] repo_path_to_use = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]
@ -88,14 +97,26 @@ class GitService:
shell_command_path, shell_command_path,
repo_path_to_use, repo_path_to_use,
message, message,
branch_name_to_use,
git_username, git_username,
git_email, git_email,
current_app.config["GIT_USER_PASSWORD"],
] ]
return cls.run_shell_command_to_get_stdout(shell_command) return cls.run_shell_command_to_get_stdout(shell_command)
@classmethod @classmethod
def check_for_configs(cls) -> None: def check_for_basic_configs(cls) -> None:
"""Check_for_basic_configs."""
if current_app.config["GIT_BRANCH"] is None:
raise MissingGitConfigsError(
"Missing config for GIT_BRANCH. "
"This is required for publishing process models"
)
@classmethod
def check_for_publish_configs(cls) -> None:
"""Check_for_configs.""" """Check_for_configs."""
cls.check_for_basic_configs()
if current_app.config["GIT_BRANCH_TO_PUBLISH_TO"] is None: if current_app.config["GIT_BRANCH_TO_PUBLISH_TO"] is None:
raise MissingGitConfigsError( raise MissingGitConfigsError(
"Missing config for GIT_BRANCH_TO_PUBLISH_TO. " "Missing config for GIT_BRANCH_TO_PUBLISH_TO. "
@ -148,7 +169,7 @@ class GitService:
@classmethod @classmethod
def handle_web_hook(cls, webhook: dict) -> bool: def handle_web_hook(cls, webhook: dict) -> bool:
"""Handle_web_hook.""" """Handle_web_hook."""
cls.check_for_configs() cls.check_for_publish_configs()
if "repository" not in webhook or "clone_url" not in webhook["repository"]: if "repository" not in webhook or "clone_url" not in webhook["repository"]:
raise InvalidGitWebhookBodyError( raise InvalidGitWebhookBodyError(
@ -184,7 +205,7 @@ class GitService:
@classmethod @classmethod
def publish(cls, process_model_id: str, branch_to_update: str) -> str: def publish(cls, process_model_id: str, branch_to_update: str) -> str:
"""Publish.""" """Publish."""
cls.check_for_configs() cls.check_for_publish_configs()
source_process_model_root = FileSystemService.root_path() source_process_model_root = FileSystemService.root_path()
source_process_model_path = os.path.join( source_process_model_path = os.path.join(
source_process_model_root, process_model_id source_process_model_root, process_model_id
@ -233,10 +254,7 @@ class GitService:
f"Request to publish changes to {process_model_id}, " f"Request to publish changes to {process_model_id}, "
f"from {g.user.username} on {current_app.config['ENV_IDENTIFIER']}" f"from {g.user.username} on {current_app.config['ENV_IDENTIFIER']}"
) )
cls.commit(commit_message, destination_process_root) cls.commit(commit_message, destination_process_root, branch_to_pull_request)
cls.run_shell_command(
["git", "push", "--set-upstream", "origin", branch_to_pull_request]
)
# build url for github page to open PR # build url for github page to open PR
git_remote = cls.run_shell_command_to_get_stdout( git_remote = cls.run_shell_command_to_get_stdout(

View File

@ -65,11 +65,11 @@ from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.task import TaskState 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_user import ActiveTaskUserModel
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.group import GroupModel
from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel
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,
@ -558,7 +558,7 @@ class ProcessInstanceProcessor:
"spiff_step": self.process_instance_model.spiff_step or 1, "spiff_step": self.process_instance_model.spiff_step or 1,
"task_json": task_json, "task_json": task_json,
"timestamp": round(time.time()), "timestamp": round(time.time()),
"completed_by_user_id": self.current_user().id, # "completed_by_user_id": self.current_user().id,
} }
def spiff_step_details(self) -> SpiffStepDetailsModel: def spiff_step_details(self) -> SpiffStepDetailsModel:
@ -569,14 +569,13 @@ class ProcessInstanceProcessor:
spiff_step=details_mapping["spiff_step"], spiff_step=details_mapping["spiff_step"],
task_json=details_mapping["task_json"], task_json=details_mapping["task_json"],
timestamp=details_mapping["timestamp"], timestamp=details_mapping["timestamp"],
completed_by_user_id=details_mapping["completed_by_user_id"], # completed_by_user_id=details_mapping["completed_by_user_id"],
) )
return details_model return details_model
def save_spiff_step_details(self, active_task: ActiveTaskModel) -> None: def save_spiff_step_details(self) -> None:
"""SaveSpiffStepDetails.""" """SaveSpiffStepDetails."""
details_model = self.spiff_step_details() details_model = self.spiff_step_details()
details_model.lane_assignment_id = active_task.lane_assignment_id
db.session.add(details_model) db.session.add(details_model)
db.session.commit() db.session.commit()
@ -637,7 +636,7 @@ class ProcessInstanceProcessor:
db.session.add(self.process_instance_model) db.session.add(self.process_instance_model)
db.session.commit() db.session.commit()
active_tasks = ActiveTaskModel.query.filter_by( human_tasks = HumanTaskModel.query.filter_by(
process_instance_id=self.process_instance_model.id process_instance_id=self.process_instance_model.id
).all() ).all()
ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks() ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks()
@ -668,14 +667,14 @@ class ProcessInstanceProcessor:
if "formUiSchemaFilename" in properties: if "formUiSchemaFilename" in properties:
ui_form_file_name = properties["formUiSchemaFilename"] ui_form_file_name = properties["formUiSchemaFilename"]
active_task = None human_task = None
for at in active_tasks: for at in human_tasks:
if at.task_id == str(ready_or_waiting_task.id): if at.task_id == str(ready_or_waiting_task.id):
active_task = at human_task = at
active_tasks.remove(at) human_tasks.remove(at)
if active_task is None: if human_task is None:
active_task = ActiveTaskModel( human_task = HumanTaskModel(
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,
form_file_name=form_file_name, form_file_name=form_file_name,
@ -687,21 +686,22 @@ class ProcessInstanceProcessor:
task_status=ready_or_waiting_task.get_state_name(), task_status=ready_or_waiting_task.get_state_name(),
lane_assignment_id=potential_owner_hash["lane_assignment_id"], lane_assignment_id=potential_owner_hash["lane_assignment_id"],
) )
db.session.add(active_task) db.session.add(human_task)
db.session.commit() db.session.commit()
for potential_owner_id in potential_owner_hash[ for potential_owner_id in potential_owner_hash[
"potential_owner_ids" "potential_owner_ids"
]: ]:
active_task_user = ActiveTaskUserModel( human_task_user = HumanTaskUserModel(
user_id=potential_owner_id, active_task_id=active_task.id user_id=potential_owner_id, human_task_id=human_task.id
) )
db.session.add(active_task_user) db.session.add(human_task_user)
db.session.commit() db.session.commit()
if len(active_tasks) > 0: if len(human_tasks) > 0:
for at in active_tasks: for at in human_tasks:
db.session.delete(at) at.completed = True
db.session.add(at)
db.session.commit() db.session.commit()
@staticmethod @staticmethod
@ -1179,11 +1179,16 @@ class ProcessInstanceProcessor:
) )
return user_tasks # type: ignore return user_tasks # type: ignore
def complete_task(self, task: SpiffTask, active_task: ActiveTaskModel) -> None: def complete_task(
self, task: SpiffTask, human_task: HumanTaskModel, user: UserModel
) -> None:
"""Complete_task.""" """Complete_task."""
self.increment_spiff_step() self.increment_spiff_step()
self.bpmn_process_instance.complete_task_from_id(task.id) self.bpmn_process_instance.complete_task_from_id(task.id)
self.save_spiff_step_details(active_task) human_task.completed_by_user_id = user.id
db.session.add(human_task)
db.session.commit()
self.save_spiff_step_details()
def get_data(self) -> dict[str, Any]: def get_data(self) -> dict[str, Any]:
"""Get_data.""" """Get_data."""

View File

@ -1,14 +1,30 @@
"""Process_instance_report_service.""" """Process_instance_report_service."""
import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
import sqlalchemy import sqlalchemy
from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from sqlalchemy import and_
from sqlalchemy import func
from sqlalchemy import or_
from sqlalchemy.orm import aliased
from sqlalchemy.orm import selectinload
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance_metadata import (
ProcessInstanceMetadataModel,
)
from spiffworkflow_backend.models.process_instance_report import ( from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel, ProcessInstanceReportModel,
) )
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
from spiffworkflow_backend.services.process_model_service import ProcessModelService
@dataclass @dataclass
@ -16,14 +32,17 @@ class ProcessInstanceReportFilter:
"""ProcessInstanceReportFilter.""" """ProcessInstanceReportFilter."""
process_model_identifier: Optional[str] = None process_model_identifier: Optional[str] = None
user_group_identifier: Optional[str] = None
start_from: Optional[int] = None start_from: Optional[int] = None
start_to: Optional[int] = None start_to: Optional[int] = None
end_from: Optional[int] = None end_from: Optional[int] = None
end_to: Optional[int] = None end_to: Optional[int] = None
process_status: Optional[list[str]] = None process_status: Optional[list[str]] = None
initiated_by_me: Optional[bool] = None initiated_by_me: Optional[bool] = None
has_terminal_status: Optional[bool] = None
with_tasks_completed_by_me: Optional[bool] = None with_tasks_completed_by_me: Optional[bool] = None
with_tasks_completed_by_my_group: Optional[bool] = None with_tasks_assigned_to_my_group: Optional[bool] = None
with_relation_to_me: Optional[bool] = None
def to_dict(self) -> dict[str, str]: def to_dict(self) -> dict[str, str]:
"""To_dict.""" """To_dict."""
@ -31,6 +50,8 @@ class ProcessInstanceReportFilter:
if self.process_model_identifier is not None: if self.process_model_identifier is not None:
d["process_model_identifier"] = self.process_model_identifier d["process_model_identifier"] = self.process_model_identifier
if self.user_group_identifier is not None:
d["user_group_identifier"] = self.user_group_identifier
if self.start_from is not None: if self.start_from is not None:
d["start_from"] = str(self.start_from) d["start_from"] = str(self.start_from)
if self.start_to is not None: if self.start_to is not None:
@ -43,14 +64,18 @@ class ProcessInstanceReportFilter:
d["process_status"] = ",".join(self.process_status) d["process_status"] = ",".join(self.process_status)
if self.initiated_by_me is not None: if self.initiated_by_me is not None:
d["initiated_by_me"] = str(self.initiated_by_me).lower() d["initiated_by_me"] = str(self.initiated_by_me).lower()
if self.has_terminal_status is not None:
d["has_terminal_status"] = str(self.has_terminal_status).lower()
if self.with_tasks_completed_by_me is not None: if self.with_tasks_completed_by_me is not None:
d["with_tasks_completed_by_me"] = str( d["with_tasks_completed_by_me"] = str(
self.with_tasks_completed_by_me self.with_tasks_completed_by_me
).lower() ).lower()
if self.with_tasks_completed_by_my_group is not None: if self.with_tasks_assigned_to_my_group is not None:
d["with_tasks_completed_by_my_group"] = str( d["with_tasks_assigned_to_my_group"] = str(
self.with_tasks_completed_by_my_group self.with_tasks_assigned_to_my_group
).lower() ).lower()
if self.with_relation_to_me is not None:
d["with_relation_to_me"] = str(self.with_relation_to_me).lower()
return d return d
@ -89,7 +114,7 @@ class ProcessInstanceReportService:
"filter_by": [], "filter_by": [],
"order_by": ["-start_in_seconds", "-id"], "order_by": ["-start_in_seconds", "-id"],
}, },
"system_report_instances_initiated_by_me": { "system_report_completed_instances_initiated_by_me": {
"columns": [ "columns": [
{"Header": "id", "accessor": "id"}, {"Header": "id", "accessor": "id"},
{ {
@ -100,28 +125,32 @@ class ProcessInstanceReportService:
{"Header": "end_in_seconds", "accessor": "end_in_seconds"}, {"Header": "end_in_seconds", "accessor": "end_in_seconds"},
{"Header": "status", "accessor": "status"}, {"Header": "status", "accessor": "status"},
], ],
"filter_by": [{"field_name": "initiated_by_me", "field_value": True}],
"order_by": ["-start_in_seconds", "-id"],
},
"system_report_instances_with_tasks_completed_by_me": {
"columns": cls.builtin_column_options(),
"filter_by": [ "filter_by": [
{"field_name": "with_tasks_completed_by_me", "field_value": True} {"field_name": "initiated_by_me", "field_value": True},
{"field_name": "has_terminal_status", "field_value": True},
], ],
"order_by": ["-start_in_seconds", "-id"], "order_by": ["-start_in_seconds", "-id"],
}, },
"system_report_instances_with_tasks_completed_by_my_groups": { "system_report_completed_instances_with_tasks_completed_by_me": {
"columns": cls.builtin_column_options(),
"filter_by": [
{"field_name": "with_tasks_completed_by_me", "field_value": True},
{"field_name": "has_terminal_status", "field_value": True},
],
"order_by": ["-start_in_seconds", "-id"],
},
"system_report_completed_instances_with_tasks_completed_by_my_groups": {
"columns": cls.builtin_column_options(), "columns": cls.builtin_column_options(),
"filter_by": [ "filter_by": [
{ {
"field_name": "with_tasks_completed_by_my_group", "field_name": "with_tasks_assigned_to_my_group",
"field_value": True, "field_value": True,
} },
{"field_name": "has_terminal_status", "field_value": True},
], ],
"order_by": ["-start_in_seconds", "-id"], "order_by": ["-start_in_seconds", "-id"],
}, },
} }
process_instance_report = ProcessInstanceReportModel( process_instance_report = ProcessInstanceReportModel(
identifier=report_identifier, identifier=report_identifier,
created_by_id=user.id, created_by_id=user.id,
@ -164,27 +193,31 @@ class ProcessInstanceReportService:
return filters[key].split(",") if key in filters else None return filters[key].split(",") if key in filters else None
process_model_identifier = filters.get("process_model_identifier") process_model_identifier = filters.get("process_model_identifier")
user_group_identifier = filters.get("user_group_identifier")
start_from = int_value("start_from") start_from = int_value("start_from")
start_to = int_value("start_to") start_to = int_value("start_to")
end_from = int_value("end_from") end_from = int_value("end_from")
end_to = int_value("end_to") end_to = int_value("end_to")
process_status = list_value("process_status") process_status = list_value("process_status")
initiated_by_me = bool_value("initiated_by_me") initiated_by_me = bool_value("initiated_by_me")
has_terminal_status = bool_value("has_terminal_status")
with_tasks_completed_by_me = bool_value("with_tasks_completed_by_me") with_tasks_completed_by_me = bool_value("with_tasks_completed_by_me")
with_tasks_completed_by_my_group = bool_value( with_tasks_assigned_to_my_group = bool_value("with_tasks_assigned_to_my_group")
"with_tasks_completed_by_my_group" with_relation_to_me = bool_value("with_relation_to_me")
)
report_filter = ProcessInstanceReportFilter( report_filter = ProcessInstanceReportFilter(
process_model_identifier, process_model_identifier,
user_group_identifier,
start_from, start_from,
start_to, start_to,
end_from, end_from,
end_to, end_to,
process_status, process_status,
initiated_by_me, initiated_by_me,
has_terminal_status,
with_tasks_completed_by_me, with_tasks_completed_by_me,
with_tasks_completed_by_my_group, with_tasks_assigned_to_my_group,
with_relation_to_me,
) )
return report_filter return report_filter
@ -194,20 +227,25 @@ class ProcessInstanceReportService:
cls, cls,
process_instance_report: ProcessInstanceReportModel, process_instance_report: ProcessInstanceReportModel,
process_model_identifier: Optional[str] = None, process_model_identifier: Optional[str] = None,
user_group_identifier: Optional[str] = None,
start_from: Optional[int] = None, start_from: Optional[int] = None,
start_to: Optional[int] = None, start_to: Optional[int] = None,
end_from: Optional[int] = None, end_from: Optional[int] = None,
end_to: Optional[int] = None, end_to: Optional[int] = None,
process_status: Optional[str] = None, process_status: Optional[str] = None,
initiated_by_me: Optional[bool] = None, initiated_by_me: Optional[bool] = None,
has_terminal_status: Optional[bool] = None,
with_tasks_completed_by_me: Optional[bool] = None, with_tasks_completed_by_me: Optional[bool] = None,
with_tasks_completed_by_my_group: Optional[bool] = None, with_tasks_assigned_to_my_group: Optional[bool] = None,
with_relation_to_me: Optional[bool] = None,
) -> ProcessInstanceReportFilter: ) -> ProcessInstanceReportFilter:
"""Filter_from_metadata_with_overrides.""" """Filter_from_metadata_with_overrides."""
report_filter = cls.filter_from_metadata(process_instance_report) report_filter = cls.filter_from_metadata(process_instance_report)
if process_model_identifier is not None: if process_model_identifier is not None:
report_filter.process_model_identifier = process_model_identifier report_filter.process_model_identifier = process_model_identifier
if user_group_identifier is not None:
report_filter.user_group_identifier = user_group_identifier
if start_from is not None: if start_from is not None:
report_filter.start_from = start_from report_filter.start_from = start_from
if start_to is not None: if start_to is not None:
@ -220,12 +258,16 @@ class ProcessInstanceReportService:
report_filter.process_status = process_status.split(",") report_filter.process_status = process_status.split(",")
if initiated_by_me is not None: if initiated_by_me is not None:
report_filter.initiated_by_me = initiated_by_me report_filter.initiated_by_me = initiated_by_me
if has_terminal_status is not None:
report_filter.has_terminal_status = has_terminal_status
if with_tasks_completed_by_me is not None: if with_tasks_completed_by_me is not None:
report_filter.with_tasks_completed_by_me = with_tasks_completed_by_me report_filter.with_tasks_completed_by_me = with_tasks_completed_by_me
if with_tasks_completed_by_my_group is not None: if with_tasks_assigned_to_my_group is not None:
report_filter.with_tasks_completed_by_my_group = ( report_filter.with_tasks_assigned_to_my_group = (
with_tasks_completed_by_my_group with_tasks_assigned_to_my_group
) )
if with_relation_to_me is not None:
report_filter.with_relation_to_me = with_relation_to_me
return report_filter return report_filter
@ -268,3 +310,204 @@ class ProcessInstanceReportService:
{"Header": "Username", "accessor": "username", "filterable": False}, {"Header": "Username", "accessor": "username", "filterable": False},
{"Header": "Status", "accessor": "status", "filterable": False}, {"Header": "Status", "accessor": "status", "filterable": False},
] ]
@classmethod
def run_process_instance_report(
cls,
report_filter: ProcessInstanceReportFilter,
process_instance_report: ProcessInstanceReportModel,
user: UserModel,
page: int = 1,
per_page: int = 100,
) -> dict:
"""Run_process_instance_report."""
process_instance_query = ProcessInstanceModel.query
# Always join that hot user table for good performance at serialization time.
process_instance_query = process_instance_query.options(
selectinload(ProcessInstanceModel.process_initiator)
)
if report_filter.process_model_identifier is not None:
process_model = ProcessModelService.get_process_model(
f"{report_filter.process_model_identifier}",
)
process_instance_query = process_instance_query.filter_by(
process_model_identifier=process_model.id
)
# this can never happen. obviously the class has the columns it defines. this is just to appease mypy.
if (
ProcessInstanceModel.start_in_seconds is None
or ProcessInstanceModel.end_in_seconds is None
):
raise (
ApiError(
error_code="unexpected_condition",
message="Something went very wrong",
status_code=500,
)
)
if report_filter.start_from is not None:
process_instance_query = process_instance_query.filter(
ProcessInstanceModel.start_in_seconds >= report_filter.start_from
)
if report_filter.start_to is not None:
process_instance_query = process_instance_query.filter(
ProcessInstanceModel.start_in_seconds <= report_filter.start_to
)
if report_filter.end_from is not None:
process_instance_query = process_instance_query.filter(
ProcessInstanceModel.end_in_seconds >= report_filter.end_from
)
if report_filter.end_to is not None:
process_instance_query = process_instance_query.filter(
ProcessInstanceModel.end_in_seconds <= report_filter.end_to
)
if report_filter.process_status is not None:
process_instance_query = process_instance_query.filter(
ProcessInstanceModel.status.in_(report_filter.process_status) # type: ignore
)
if report_filter.initiated_by_me is True:
process_instance_query = process_instance_query.filter_by(
process_initiator=user
)
if report_filter.has_terminal_status is True:
process_instance_query = process_instance_query.filter(
ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore
)
if (
not report_filter.with_tasks_completed_by_me
and not report_filter.with_tasks_assigned_to_my_group
and report_filter.with_relation_to_me is True
):
process_instance_query = process_instance_query.outerjoin(
HumanTaskModel
).outerjoin(
HumanTaskUserModel,
and_(
HumanTaskModel.id == HumanTaskUserModel.human_task_id,
HumanTaskUserModel.user_id == user.id,
),
)
process_instance_query = process_instance_query.filter(
or_(
HumanTaskUserModel.id.is_not(None),
ProcessInstanceModel.process_initiator_id == user.id,
)
)
if report_filter.with_tasks_completed_by_me is True:
process_instance_query = process_instance_query.filter(
ProcessInstanceModel.process_initiator_id != user.id
)
process_instance_query = process_instance_query.join(
HumanTaskModel,
and_(
HumanTaskModel.process_instance_id == ProcessInstanceModel.id,
HumanTaskModel.completed_by_user_id == user.id,
),
)
if report_filter.with_tasks_assigned_to_my_group is True:
group_model_join_conditions = [GroupModel.id == HumanTaskModel.lane_assignment_id]
if report_filter.user_group_identifier:
group_model_join_conditions.append(GroupModel.identifier == report_filter.user_group_identifier)
process_instance_query = process_instance_query.join(HumanTaskModel)
process_instance_query = process_instance_query.join(
GroupModel,
and_(*group_model_join_conditions)
)
process_instance_query = process_instance_query.join(
UserGroupAssignmentModel,
UserGroupAssignmentModel.group_id == GroupModel.id,
)
process_instance_query = process_instance_query.filter(
UserGroupAssignmentModel.user_id == user.id
)
instance_metadata_aliases = {}
stock_columns = ProcessInstanceReportService.get_column_names_for_model(
ProcessInstanceModel
)
for column in process_instance_report.report_metadata["columns"]:
if column["accessor"] in stock_columns:
continue
instance_metadata_alias = aliased(ProcessInstanceMetadataModel)
instance_metadata_aliases[column["accessor"]] = instance_metadata_alias
filter_for_column = None
if "filter_by" in process_instance_report.report_metadata:
filter_for_column = next(
(
f
for f in process_instance_report.report_metadata["filter_by"]
if f["field_name"] == column["accessor"]
),
None,
)
isouter = True
conditions = [
ProcessInstanceModel.id == instance_metadata_alias.process_instance_id,
instance_metadata_alias.key == column["accessor"],
]
if filter_for_column:
isouter = False
conditions.append(
instance_metadata_alias.value == filter_for_column["field_value"]
)
process_instance_query = process_instance_query.join(
instance_metadata_alias, and_(*conditions), isouter=isouter
).add_columns(
func.max(instance_metadata_alias.value).label(column["accessor"])
)
order_by_query_array = []
order_by_array = process_instance_report.report_metadata["order_by"]
if len(order_by_array) < 1:
order_by_array = ProcessInstanceReportModel.default_order_by()
for order_by_option in order_by_array:
attribute = re.sub("^-", "", order_by_option)
if attribute in stock_columns:
if order_by_option.startswith("-"):
order_by_query_array.append(
getattr(ProcessInstanceModel, attribute).desc()
)
else:
order_by_query_array.append(
getattr(ProcessInstanceModel, attribute).asc()
)
elif attribute in instance_metadata_aliases:
if order_by_option.startswith("-"):
order_by_query_array.append(
func.max(instance_metadata_aliases[attribute].value).desc()
)
else:
order_by_query_array.append(
func.max(instance_metadata_aliases[attribute].value).asc()
)
# return process_instance_query
process_instances = (
process_instance_query.group_by(ProcessInstanceModel.id)
.add_columns(ProcessInstanceModel.id)
.order_by(*order_by_query_array)
.paginate(page=page, per_page=per_page, error_out=False)
)
results = ProcessInstanceReportService.add_metadata_columns_to_process_instance(
process_instances.items, process_instance_report.report_metadata["columns"]
)
response_json = {
"report": process_instance_report,
"results": results,
"filters": report_filter.to_dict(),
"pagination": {
"count": len(results),
"total": process_instances.total,
"pages": process_instances.pages,
},
}
return response_json

View File

@ -8,7 +8,7 @@ from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from spiffworkflow_backend.models.active_task import ActiveTaskModel from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceApi from spiffworkflow_backend.models.process_instance import ProcessInstanceApi
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
@ -196,7 +196,7 @@ class ProcessInstanceService:
spiff_task: SpiffTask, spiff_task: SpiffTask,
data: dict[str, Any], data: dict[str, Any],
user: UserModel, user: UserModel,
active_task: ActiveTaskModel, human_task: HumanTaskModel,
) -> None: ) -> None:
"""All the things that need to happen when we complete a form. """All the things that need to happen when we complete a form.
@ -210,7 +210,7 @@ class ProcessInstanceService:
dot_dct = ProcessInstanceService.create_dot_dict(data) dot_dct = ProcessInstanceService.create_dot_dict(data)
spiff_task.update_data(dot_dct) spiff_task.update_data(dot_dct)
# ProcessInstanceService.post_process_form(spiff_task) # some properties may update the data store. # ProcessInstanceService.post_process_form(spiff_task) # some properties may update the data store.
processor.complete_task(spiff_task, active_task) processor.complete_task(spiff_task, human_task, user=user)
processor.do_engine_steps(save=True) processor.do_engine_steps(save=True)
@staticmethod @staticmethod

View File

@ -223,7 +223,7 @@ class ProcessModelService(FileSystemService):
user = UserService.current_user() user = UserService.current_user()
new_process_model_list = [] new_process_model_list = []
for process_model in process_models: for process_model in process_models:
uri = f"/v1.0/process-models/{process_model.id.replace('/', ':')}/process-instances" uri = f"/v1.0/process-instances/{process_model.id.replace('/', ':')}"
result = AuthorizationService.user_has_permission( result = AuthorizationService.user_has_permission(
user=user, permission="create", target_uri=uri user=user, permission="create", target_uri=uri
) )

View File

@ -31,7 +31,6 @@ class ServiceTaskDelegate:
if value.startswith(secret_prefix): if value.startswith(secret_prefix):
key = value.removeprefix(secret_prefix) key = value.removeprefix(secret_prefix)
secret = SecretService().get_secret(key) secret = SecretService().get_secret(key)
assert secret # noqa: S101
return secret.value return secret.value
file_prefix = "file:" file_prefix = "file:"

View File

@ -7,9 +7,9 @@ from flask import g
from flask_bpmn.api.api_error import ApiError from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from spiffworkflow_backend.models.active_task import ActiveTaskModel
from spiffworkflow_backend.models.active_task_user import ActiveTaskUserModel
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel
from spiffworkflow_backend.models.principal import PrincipalModel from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
@ -192,15 +192,15 @@ class UserService:
return None return None
@classmethod @classmethod
def add_user_to_active_tasks_if_appropriate(cls, user: UserModel) -> None: def add_user_to_human_tasks_if_appropriate(cls, user: UserModel) -> None:
"""Add_user_to_active_tasks_if_appropriate.""" """Add_user_to_human_tasks_if_appropriate."""
group_ids = [g.id for g in user.groups] group_ids = [g.id for g in user.groups]
active_tasks = ActiveTaskModel.query.filter( human_tasks = HumanTaskModel.query.filter(
ActiveTaskModel.lane_assignment_id.in_(group_ids) # type: ignore HumanTaskModel.lane_assignment_id.in_(group_ids) # type: ignore
).all() ).all()
for active_task in active_tasks: for human_task in human_tasks:
active_task_user = ActiveTaskUserModel( human_task_user = HumanTaskUserModel(
user_id=user.id, active_task_id=active_task.id user_id=user.id, human_task_id=human_task.id
) )
db.session.add(active_task_user) db.session.add(human_task_user)
db.session.commit() db.session.commit()

View File

@ -243,7 +243,7 @@ class BaseTest:
return file return file
@staticmethod @staticmethod
def create_process_instance_from_process_model_id( def create_process_instance_from_process_model_id_with_api(
client: FlaskClient, client: FlaskClient,
test_process_model_id: str, test_process_model_id: str,
headers: Dict[str, str], headers: Dict[str, str],

View File

@ -45,7 +45,7 @@ class TestLoggingService(BaseTest):
user=with_super_admin_user, user=with_super_admin_user,
) )
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
assert response.json is not None assert response.json is not None

View File

@ -38,7 +38,7 @@ class TestNestedGroups(BaseTest):
bpmn_file_name=bpmn_file_name, bpmn_file_name=bpmn_file_name,
bpmn_file_location=bpmn_file_location, bpmn_file_location=bpmn_file_location,
) )
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, client,
process_model_identifier, process_model_identifier,
self.logged_in_headers(with_super_admin_user), self.logged_in_headers(with_super_admin_user),
@ -99,7 +99,7 @@ class TestNestedGroups(BaseTest):
bpmn_file_name=bpmn_file_name, bpmn_file_name=bpmn_file_name,
bpmn_file_location=bpmn_file_location, bpmn_file_location=bpmn_file_location,
) )
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, client,
process_model_identifier, process_model_identifier,
self.logged_in_headers(with_super_admin_user), self.logged_in_headers(with_super_admin_user),

View File

@ -15,8 +15,8 @@ from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
ProcessEntityNotFoundError, ProcessEntityNotFoundError,
) )
from spiffworkflow_backend.models.active_task import ActiveTaskModel
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.process_group import ProcessGroup from spiffworkflow_backend.models.process_group import ProcessGroup
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
@ -284,7 +284,7 @@ class TestProcessApi(BaseTest):
) )
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
# create an instance from a model # create an instance from a model
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
@ -1072,7 +1072,7 @@ class TestProcessApi(BaseTest):
"""Test_process_instance_create.""" """Test_process_instance_create."""
test_process_model_id = "runs_without_input/sample" test_process_model_id = "runs_without_input/sample"
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, test_process_model_id, headers client, test_process_model_id, headers
) )
assert response.json is not None assert response.json is not None
@ -1102,7 +1102,7 @@ class TestProcessApi(BaseTest):
) )
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
assert response.json is not None assert response.json is not None
@ -1144,7 +1144,7 @@ class TestProcessApi(BaseTest):
self.modify_process_identifier_for_path_param(process_model_identifier) self.modify_process_identifier_for_path_param(process_model_identifier)
) )
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
create_response = self.create_process_instance_from_process_model_id( create_response = self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
assert create_response.json is not None assert create_response.json is not None
@ -1191,7 +1191,7 @@ class TestProcessApi(BaseTest):
self.modify_process_identifier_for_path_param(process_model_identifier) self.modify_process_identifier_for_path_param(process_model_identifier)
) )
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
create_response = self.create_process_instance_from_process_model_id( create_response = self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
assert create_response.json is not None assert create_response.json is not None
@ -1299,7 +1299,7 @@ class TestProcessApi(BaseTest):
"andThis": "another_item_non_key", "andThis": "another_item_non_key",
} }
} }
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, client,
process_model_identifier, process_model_identifier,
self.logged_in_headers(with_super_admin_user), self.logged_in_headers(with_super_admin_user),
@ -1359,7 +1359,7 @@ class TestProcessApi(BaseTest):
bpmn_file_location=bpmn_file_location, bpmn_file_location=bpmn_file_location,
) )
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, client,
process_model_identifier, process_model_identifier,
self.logged_in_headers(with_super_admin_user), self.logged_in_headers(with_super_admin_user),
@ -1375,7 +1375,7 @@ class TestProcessApi(BaseTest):
assert response.json is not None assert response.json is not None
response = client.post( 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), headers=self.logged_in_headers(with_super_admin_user),
) )
assert response.status_code == 200 assert response.status_code == 200
@ -1396,20 +1396,18 @@ class TestProcessApi(BaseTest):
) -> None: ) -> None:
"""Test_process_instance_delete.""" """Test_process_instance_delete."""
process_group_id = "my_process_group" process_group_id = "my_process_group"
process_model_id = "user_task" process_model_id = "sample"
bpmn_file_name = "user_task.bpmn" bpmn_file_location = "sample"
bpmn_file_location = "user_task"
process_model_identifier = self.create_group_and_model_with_bpmn( process_model_identifier = self.create_group_and_model_with_bpmn(
client, client,
with_super_admin_user, with_super_admin_user,
process_group_id=process_group_id, process_group_id=process_group_id,
process_model_id=process_model_id, process_model_id=process_model_id,
bpmn_file_name=bpmn_file_name,
bpmn_file_location=bpmn_file_location, bpmn_file_location=bpmn_file_location,
) )
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
assert response.json is not None assert response.json is not None
@ -1420,11 +1418,13 @@ class TestProcessApi(BaseTest):
headers=self.logged_in_headers(with_super_admin_user), headers=self.logged_in_headers(with_super_admin_user),
) )
assert response.json is not None assert response.json is not None
assert response.status_code == 200
delete_response = client.delete( delete_response = client.delete(
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}", 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), headers=self.logged_in_headers(with_super_admin_user),
) )
assert delete_response.json["ok"] is True
assert delete_response.status_code == 200 assert delete_response.status_code == 200
def test_task_show( def test_task_show(
@ -1448,7 +1448,7 @@ class TestProcessApi(BaseTest):
) )
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
assert response.json is not None assert response.json is not None
@ -1462,15 +1462,15 @@ class TestProcessApi(BaseTest):
assert response.json is not None assert response.json is not None
assert response.json["next_task"] is not None assert response.json["next_task"] is not None
active_tasks = ( human_tasks = (
db.session.query(ActiveTaskModel) db.session.query(HumanTaskModel)
.filter(ActiveTaskModel.process_instance_id == process_instance_id) .filter(HumanTaskModel.process_instance_id == process_instance_id)
.all() .all()
) )
assert len(active_tasks) == 1 assert len(human_tasks) == 1
active_task = active_tasks[0] human_task = human_tasks[0]
response = client.get( response = client.get(
f"/v1.0/tasks/{process_instance_id}/{active_task.task_id}", f"/v1.0/tasks/{process_instance_id}/{human_task.task_id}",
headers=self.logged_in_headers(with_super_admin_user), headers=self.logged_in_headers(with_super_admin_user),
) )
assert response.json is not None assert response.json is not None
@ -1499,7 +1499,7 @@ class TestProcessApi(BaseTest):
) )
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
self.create_process_instance_from_process_model_id( self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
@ -1546,19 +1546,19 @@ class TestProcessApi(BaseTest):
bpmn_file_location=bpmn_file_location, bpmn_file_location=bpmn_file_location,
) )
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
self.create_process_instance_from_process_model_id( self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
self.create_process_instance_from_process_model_id( self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
self.create_process_instance_from_process_model_id( self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
self.create_process_instance_from_process_model_id( self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
self.create_process_instance_from_process_model_id( self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
@ -1872,7 +1872,7 @@ class TestProcessApi(BaseTest):
) -> Any: ) -> Any:
"""Setup_testing_instance.""" """Setup_testing_instance."""
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, process_model_id, headers client, process_model_id, headers
) )
process_instance = response.json process_instance = response.json
@ -2195,7 +2195,7 @@ class TestProcessApi(BaseTest):
# process_group_id="finance", # process_group_id="finance",
# ) # )
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, client,
# process_model.process_group_id, # process_model.process_group_id,
process_model_identifier, process_model_identifier,
@ -2404,7 +2404,7 @@ class TestProcessApi(BaseTest):
) )
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
assert response.json is not None assert response.json is not None
@ -2421,7 +2421,7 @@ class TestProcessApi(BaseTest):
assert process_instance.status == "user_input_required" assert process_instance.status == "user_input_required"
client.post( 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), headers=self.logged_in_headers(with_super_admin_user),
) )
process_instance = ProcessInstanceService().get_process_instance( process_instance = ProcessInstanceService().get_process_instance(
@ -2429,15 +2429,25 @@ class TestProcessApi(BaseTest):
) )
assert process_instance.status == "suspended" assert process_instance.status == "suspended"
# TODO: Why can I run a suspended process instance?
response = client.post( response = client.post(
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run", 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), 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'] response = client.post(
f"/v1.0/process-instance-resume/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
print("test_process_instance_suspend") 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( def test_script_unit_test_run(
self, self,
@ -2550,7 +2560,7 @@ class TestProcessApi(BaseTest):
f"/v1.0/process-models/{modified_original_process_model_id}/move?new_location={new_location}", f"/v1.0/process-models/{modified_original_process_model_id}/move?new_location={new_location}",
headers=self.logged_in_headers(with_super_admin_user), headers=self.logged_in_headers(with_super_admin_user),
) )
assert response.status_code == 201 assert response.status_code == 200
assert response.json["id"] == new_process_model_path assert response.json["id"] == new_process_model_path
# make sure the original model does not exist # make sure the original model does not exist
@ -2595,7 +2605,7 @@ class TestProcessApi(BaseTest):
f"/v1.0/process-groups/{modified_original_process_group_id}/move?new_location={new_location}", f"/v1.0/process-groups/{modified_original_process_group_id}/move?new_location={new_location}",
headers=self.logged_in_headers(with_super_admin_user), headers=self.logged_in_headers(with_super_admin_user),
) )
assert response.status_code == 201 assert response.status_code == 200
assert response.json["id"] == new_sub_path assert response.json["id"] == new_sub_path
# make sure the original subgroup does not exist # make sure the original subgroup does not exist

View File

@ -68,9 +68,9 @@ class TestGetLocaltime(BaseTest):
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True) processor.do_engine_steps(save=True)
active_task = process_instance.active_tasks[0] human_task = process_instance.human_tasks[0]
spiff_task = processor.__class__.get_task_by_bpmn_identifier( spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance human_task.task_name, processor.bpmn_process_instance
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
@ -78,12 +78,12 @@ class TestGetLocaltime(BaseTest):
spiff_task, spiff_task,
{"timezone": "US/Pacific"}, {"timezone": "US/Pacific"},
initiator_user, initiator_user,
active_task, human_task,
) )
active_task = process_instance.active_tasks[0] human_task = process_instance.human_tasks[0]
spiff_task = processor.__class__.get_task_by_bpmn_identifier( spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance human_task.task_name, processor.bpmn_process_instance
) )
assert spiff_task assert spiff_task

View File

@ -90,14 +90,14 @@ class TestAuthorizationService(BaseTest):
users["testuser2"], "read", "/v1.0/process-groups/" users["testuser2"], "read", "/v1.0/process-groups/"
) )
def test_user_can_be_added_to_active_task_on_first_login( def test_user_can_be_added_to_human_task_on_first_login(
self, self,
app: Flask, app: Flask,
client: FlaskClient, client: FlaskClient,
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel, with_super_admin_user: UserModel,
) -> None: ) -> None:
"""Test_user_can_be_added_to_active_task_on_first_login.""" """Test_user_can_be_added_to_human_task_on_first_login."""
initiator_user = self.find_or_create_user("initiator_user") initiator_user = self.find_or_create_user("initiator_user")
assert initiator_user.principal is not None assert initiator_user.principal is not None
# to ensure there is a user that can be assigned to the task # to ensure there is a user that can be assigned to the task
@ -121,21 +121,21 @@ class TestAuthorizationService(BaseTest):
) )
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True) processor.do_engine_steps(save=True)
active_task = process_instance.active_tasks[0] human_task = process_instance.human_tasks[0]
spiff_task = processor.__class__.get_task_by_bpmn_identifier( spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance human_task.task_name, processor.bpmn_process_instance
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user, active_task processor, spiff_task, {}, initiator_user, human_task
) )
active_task = process_instance.active_tasks[0] human_task = process_instance.human_tasks[0]
spiff_task = processor.__class__.get_task_by_bpmn_identifier( spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance human_task.task_name, processor.bpmn_process_instance
) )
finance_user = AuthorizationService.create_user_from_sign_in( finance_user = AuthorizationService.create_user_from_sign_in(
{"username": "testuser2", "sub": "open_id"} {"username": "testuser2", "sub": "open_id"}
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user, active_task processor, spiff_task, {}, finance_user, human_task
) )

View File

@ -37,7 +37,7 @@ class TestDotNotation(BaseTest):
) )
headers = self.logged_in_headers(with_super_admin_user) headers = self.logged_in_headers(with_super_admin_user)
response = self.create_process_instance_from_process_model_id( response = self.create_process_instance_from_process_model_id_with_api(
client, process_model_identifier, headers client, process_model_identifier, headers
) )
process_instance_id = response.json["id"] process_instance_id = response.json["id"]
@ -47,7 +47,7 @@ class TestDotNotation(BaseTest):
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True) processor.do_engine_steps(save=True)
active_task = process_instance.active_tasks[0] human_task = process_instance.human_tasks[0]
user_task = processor.get_ready_user_tasks()[0] user_task = processor.get_ready_user_tasks()[0]
form_data = { form_data = {
@ -58,7 +58,7 @@ class TestDotNotation(BaseTest):
"invoice.dueDate": "09/30/2022", "invoice.dueDate": "09/30/2022",
} }
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, user_task, form_data, with_super_admin_user, active_task processor, user_task, form_data, with_super_admin_user, human_task
) )
expected = { expected = {

View File

@ -49,14 +49,14 @@ class TestProcessInstanceProcessor(BaseTest):
== "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( def test_sets_permission_correctly_on_human_task(
self, self,
app: Flask, app: Flask,
client: FlaskClient, client: FlaskClient,
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel, with_super_admin_user: UserModel,
) -> None: ) -> None:
"""Test_sets_permission_correctly_on_active_task.""" """Test_sets_permission_correctly_on_human_task."""
self.create_process_group( self.create_process_group(
client, with_super_admin_user, "test_group", "test_group" client, with_super_admin_user, "test_group", "test_group"
) )
@ -80,63 +80,63 @@ class TestProcessInstanceProcessor(BaseTest):
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True) processor.do_engine_steps(save=True)
assert len(process_instance.active_tasks) == 1 assert len(process_instance.human_tasks) == 1
active_task = process_instance.active_tasks[0] human_task = process_instance.human_tasks[0]
assert active_task.lane_assignment_id is None assert human_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 1 assert len(human_task.potential_owners) == 1
assert active_task.potential_owners[0] == initiator_user assert human_task.potential_owners[0] == initiator_user
spiff_task = processor.__class__.get_task_by_bpmn_identifier( spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance human_task.task_name, processor.bpmn_process_instance
) )
with pytest.raises(UserDoesNotHaveAccessToTaskError): with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user, active_task processor, spiff_task, {}, finance_user, human_task
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user, active_task processor, spiff_task, {}, initiator_user, human_task
) )
assert len(process_instance.active_tasks) == 1 assert len(process_instance.human_tasks) == 1
active_task = process_instance.active_tasks[0] human_task = process_instance.human_tasks[0]
assert active_task.lane_assignment_id == finance_group.id assert human_task.lane_assignment_id == finance_group.id
assert len(active_task.potential_owners) == 1 assert len(human_task.potential_owners) == 1
assert active_task.potential_owners[0] == finance_user assert human_task.potential_owners[0] == finance_user
spiff_task = processor.__class__.get_task_by_bpmn_identifier( spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance human_task.task_name, processor.bpmn_process_instance
) )
with pytest.raises(UserDoesNotHaveAccessToTaskError): with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user, active_task processor, spiff_task, {}, initiator_user, human_task
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user, active_task processor, spiff_task, {}, finance_user, human_task
) )
assert len(process_instance.active_tasks) == 1 assert len(process_instance.human_tasks) == 1
active_task = process_instance.active_tasks[0] human_task = process_instance.human_tasks[0]
assert active_task.lane_assignment_id is None assert human_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 1 assert len(human_task.potential_owners) == 1
assert active_task.potential_owners[0] == initiator_user assert human_task.potential_owners[0] == initiator_user
spiff_task = processor.__class__.get_task_by_bpmn_identifier( spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance human_task.task_name, processor.bpmn_process_instance
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user, active_task processor, spiff_task, {}, initiator_user, human_task
) )
assert process_instance.status == ProcessInstanceStatus.complete.value assert process_instance.status == ProcessInstanceStatus.complete.value
def test_sets_permission_correctly_on_active_task_when_using_dict( def test_sets_permission_correctly_on_human_task_when_using_dict(
self, self,
app: Flask, app: Flask,
client: FlaskClient, client: FlaskClient,
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel, with_super_admin_user: UserModel,
) -> None: ) -> None:
"""Test_sets_permission_correctly_on_active_task_when_using_dict.""" """Test_sets_permission_correctly_on_human_task_when_using_dict."""
self.create_process_group( self.create_process_group(
client, with_super_admin_user, "test_group", "test_group" client, with_super_admin_user, "test_group", "test_group"
) )
@ -163,94 +163,97 @@ class TestProcessInstanceProcessor(BaseTest):
processor.do_engine_steps(save=True) processor.do_engine_steps(save=True)
processor.save() processor.save()
assert len(process_instance.active_tasks) == 1 assert len(process_instance.human_tasks) == 1
active_task = process_instance.active_tasks[0] human_task = process_instance.human_tasks[0]
assert active_task.lane_assignment_id is None assert human_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 1 assert len(human_task.potential_owners) == 1
assert active_task.potential_owners[0] == initiator_user assert human_task.potential_owners[0] == initiator_user
spiff_task = processor.__class__.get_task_by_bpmn_identifier( spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance human_task.task_name, processor.bpmn_process_instance
) )
with pytest.raises(UserDoesNotHaveAccessToTaskError): with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user_three, active_task processor, spiff_task, {}, finance_user_three, human_task
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user, active_task processor, spiff_task, {}, initiator_user, human_task
) )
assert human_task.completed_by_user_id == initiator_user.id
assert len(process_instance.active_tasks) == 1 assert len(process_instance.human_tasks) == 1
active_task = process_instance.active_tasks[0] human_task = process_instance.human_tasks[0]
assert active_task.lane_assignment_id is None assert human_task.lane_assignment_id is None
assert len(active_task.potential_owners) == 2 assert len(human_task.potential_owners) == 2
assert active_task.potential_owners == [finance_user_three, finance_user_four] assert human_task.potential_owners == [finance_user_three, finance_user_four]
spiff_task = processor.__class__.get_task_by_bpmn_identifier( spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance human_task.task_name, processor.bpmn_process_instance
) )
with pytest.raises(UserDoesNotHaveAccessToTaskError): with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user, active_task processor, spiff_task, {}, initiator_user, human_task
) )
g.user = finance_user_three g.user = finance_user_three
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user_three, active_task processor, spiff_task, {}, finance_user_three, human_task
) )
assert len(process_instance.active_tasks) == 1 assert human_task.completed_by_user_id == finance_user_three.id
active_task = process_instance.active_tasks[0] assert len(process_instance.human_tasks) == 1
assert active_task.lane_assignment_id is None human_task = process_instance.human_tasks[0]
assert len(active_task.potential_owners) == 1 assert human_task.lane_assignment_id is None
assert active_task.potential_owners[0] == finance_user_four assert len(human_task.potential_owners) == 1
assert human_task.potential_owners[0] == finance_user_four
spiff_task = processor.__class__.get_task_by_bpmn_identifier( spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance human_task.task_name, processor.bpmn_process_instance
) )
with pytest.raises(UserDoesNotHaveAccessToTaskError): with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user, active_task processor, spiff_task, {}, initiator_user, human_task
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user_four, active_task processor, spiff_task, {}, finance_user_four, human_task
) )
assert len(process_instance.active_tasks) == 1 assert human_task.completed_by_user_id == finance_user_four.id
active_task = process_instance.active_tasks[0] assert len(process_instance.human_tasks) == 1
assert active_task.lane_assignment_id is None human_task = process_instance.human_tasks[0]
assert len(active_task.potential_owners) == 1 assert human_task.lane_assignment_id is None
assert active_task.potential_owners[0] == initiator_user assert len(human_task.potential_owners) == 1
assert human_task.potential_owners[0] == initiator_user
spiff_task = processor.__class__.get_task_by_bpmn_identifier( spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance human_task.task_name, processor.bpmn_process_instance
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user, active_task processor, spiff_task, {}, initiator_user, human_task
) )
assert len(process_instance.active_tasks) == 1 assert len(process_instance.human_tasks) == 1
active_task = process_instance.active_tasks[0] human_task = process_instance.human_tasks[0]
spiff_task = processor.__class__.get_task_by_bpmn_identifier( spiff_task = processor.__class__.get_task_by_bpmn_identifier(
active_task.task_name, processor.bpmn_process_instance human_task.task_name, processor.bpmn_process_instance
) )
with pytest.raises(UserDoesNotHaveAccessToTaskError): with pytest.raises(UserDoesNotHaveAccessToTaskError):
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, initiator_user, active_task processor, spiff_task, {}, initiator_user, human_task
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, testadmin1, active_task processor, spiff_task, {}, testadmin1, human_task
) )
assert process_instance.status == ProcessInstanceStatus.complete.value assert process_instance.status == ProcessInstanceStatus.complete.value
def test_does_not_recreate_active_tasks_on_multiple_saves( def test_does_not_recreate_human_tasks_on_multiple_saves(
self, self,
app: Flask, app: Flask,
client: FlaskClient, client: FlaskClient,
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel, with_super_admin_user: UserModel,
) -> None: ) -> None:
"""Test_sets_permission_correctly_on_active_task_when_using_dict.""" """Test_does_not_recreate_human_tasks_on_multiple_saves."""
self.create_process_group( self.create_process_group(
client, with_super_admin_user, "test_group", "test_group" client, with_super_admin_user, "test_group", "test_group"
) )
@ -273,11 +276,11 @@ class TestProcessInstanceProcessor(BaseTest):
) )
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True) processor.do_engine_steps(save=True)
assert len(process_instance.active_tasks) == 1 assert len(process_instance.human_tasks) == 1
initial_active_task_id = process_instance.active_tasks[0].id initial_human_task_id = process_instance.human_tasks[0].id
# save again to ensure we go attempt to process the active tasks again # save again to ensure we go attempt to process the human tasks again
processor.save() processor.save()
assert len(process_instance.active_tasks) == 1 assert len(process_instance.human_tasks) == 1
assert initial_active_task_id == process_instance.active_tasks[0].id assert initial_human_task_id == process_instance.human_tasks[0].id

View File

@ -3,8 +3,12 @@ from typing import Optional
from flask import Flask from flask import Flask
from flask.testing import FlaskClient from flask.testing import FlaskClient
from flask_bpmn.models.db import db
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.models.group import GroupModel
from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.process_instance_report import ( from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel, ProcessInstanceReportModel,
) )
@ -15,6 +19,7 @@ from spiffworkflow_backend.services.process_instance_report_service import (
from spiffworkflow_backend.services.process_instance_report_service import ( from spiffworkflow_backend.services.process_instance_report_service import (
ProcessInstanceReportService, ProcessInstanceReportService,
) )
from spiffworkflow_backend.services.user_service import UserService
class TestProcessInstanceReportFilter(BaseTest): class TestProcessInstanceReportFilter(BaseTest):
@ -122,13 +127,13 @@ class TestProcessInstanceReportService(BaseTest):
report_metadata=report_metadata, report_metadata=report_metadata,
) )
return ProcessInstanceReportService.filter_from_metadata_with_overrides( return ProcessInstanceReportService.filter_from_metadata_with_overrides(
report, process_instance_report=report,
process_model_identifier, process_model_identifier=process_model_identifier,
start_from, start_from=start_from,
start_to, start_to=start_to,
end_from, end_from=end_from,
end_to, end_to=end_to,
process_status, process_status=process_status,
) )
def _filter_by_dict_from_metadata(self, report_metadata: dict) -> dict[str, str]: def _filter_by_dict_from_metadata(self, report_metadata: dict) -> dict[str, str]:
@ -743,3 +748,383 @@ class TestProcessInstanceReportService(BaseTest):
assert report_filter.end_from is None assert report_filter.end_from is None
assert report_filter.end_to is None assert report_filter.end_to is None
assert report_filter.process_status == ["sue"] assert report_filter.process_status == ["sue"]
def test_can_filter_by_completed_instances_initiated_by_me(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_can_filter_by_completed_instances_initiated_by_me."""
process_model_id = "runs_without_input/sample"
bpmn_file_location = "sample"
process_model = load_test_spec(
process_model_id,
process_model_source_directory=bpmn_file_location,
)
user_one = self.find_or_create_user(username="user_one")
user_two = self.find_or_create_user(username="user_two")
# Several processes to ensure they do not return in the result
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_one
)
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_one
)
self.create_process_instance_from_process_model(
process_model=process_model, status="waiting", user=user_one
)
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_two
)
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_two
)
process_instance_report = ProcessInstanceReportService.report_with_identifier(
user=user_one,
report_identifier="system_report_completed_instances_initiated_by_me",
)
report_filter = (
ProcessInstanceReportService.filter_from_metadata_with_overrides(
process_instance_report=process_instance_report,
process_model_identifier=process_model.id,
)
)
response_json = ProcessInstanceReportService.run_process_instance_report(
report_filter=report_filter,
process_instance_report=process_instance_report,
user=user_one,
)
assert len(response_json["results"]) == 2
assert response_json["results"][0]["process_initiator_id"] == user_one.id
assert response_json["results"][1]["process_initiator_id"] == user_one.id
assert response_json["results"][0]["status"] == "complete"
assert response_json["results"][1]["status"] == "complete"
def test_can_filter_by_completed_instances_with_tasks_completed_by_me(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_can_filter_by_completed_instances_with_tasks_completed_by_me."""
process_model_id = "runs_without_input/sample"
bpmn_file_location = "sample"
process_model = load_test_spec(
process_model_id,
process_model_source_directory=bpmn_file_location,
)
user_one = self.find_or_create_user(username="user_one")
user_two = self.find_or_create_user(username="user_two")
# Several processes to ensure they do not return in the result
process_instance_created_by_user_one_one = (
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_one
)
)
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_one
)
process_instance_created_by_user_one_three = (
self.create_process_instance_from_process_model(
process_model=process_model, status="waiting", user=user_one
)
)
process_instance_created_by_user_two_one = (
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_two
)
)
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_two
)
self.create_process_instance_from_process_model(
process_model=process_model, status="waiting", user=user_two
)
human_task_for_user_one_one = HumanTaskModel(
process_instance_id=process_instance_created_by_user_one_one.id,
completed_by_user_id=user_one.id,
)
human_task_for_user_one_two = HumanTaskModel(
process_instance_id=process_instance_created_by_user_two_one.id,
completed_by_user_id=user_one.id,
)
human_task_for_user_one_three = HumanTaskModel(
process_instance_id=process_instance_created_by_user_one_three.id,
completed_by_user_id=user_one.id,
)
human_task_for_user_two_one = HumanTaskModel(
process_instance_id=process_instance_created_by_user_one_one.id,
completed_by_user_id=user_two.id,
)
human_task_for_user_two_two = HumanTaskModel(
process_instance_id=process_instance_created_by_user_two_one.id,
completed_by_user_id=user_two.id,
)
human_task_for_user_two_three = HumanTaskModel(
process_instance_id=process_instance_created_by_user_one_three.id,
completed_by_user_id=user_two.id,
)
db.session.add(human_task_for_user_one_one)
db.session.add(human_task_for_user_one_two)
db.session.add(human_task_for_user_one_three)
db.session.add(human_task_for_user_two_one)
db.session.add(human_task_for_user_two_two)
db.session.add(human_task_for_user_two_three)
db.session.commit()
process_instance_report = ProcessInstanceReportService.report_with_identifier(
user=user_one,
report_identifier="system_report_completed_instances_with_tasks_completed_by_me",
)
report_filter = (
ProcessInstanceReportService.filter_from_metadata_with_overrides(
process_instance_report=process_instance_report,
process_model_identifier=process_model.id,
)
)
response_json = ProcessInstanceReportService.run_process_instance_report(
report_filter=report_filter,
process_instance_report=process_instance_report,
user=user_one,
)
assert len(response_json["results"]) == 1
assert response_json["results"][0]["process_initiator_id"] == user_two.id
assert (
response_json["results"][0]["id"]
== process_instance_created_by_user_two_one.id
)
assert response_json["results"][0]["status"] == "complete"
def test_can_filter_by_completed_instances_with_tasks_completed_by_my_groups(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_can_filter_by_completed_instances_with_tasks_completed_by_my_groups."""
process_model_id = "runs_without_input/sample"
bpmn_file_location = "sample"
process_model = load_test_spec(
process_model_id,
process_model_source_directory=bpmn_file_location,
)
user_group_one = GroupModel(identifier="group_one")
user_group_two = GroupModel(identifier="group_two")
db.session.add(user_group_one)
db.session.add(user_group_two)
db.session.commit()
user_one = self.find_or_create_user(username="user_one")
user_two = self.find_or_create_user(username="user_two")
user_three = self.find_or_create_user(username="user_three")
UserService.add_user_to_group(user_one, user_group_one)
UserService.add_user_to_group(user_two, user_group_one)
UserService.add_user_to_group(user_three, user_group_two)
# Several processes to ensure they do not return in the result
process_instance_created_by_user_one_one = (
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_one
)
)
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_one
)
process_instance_created_by_user_one_three = (
self.create_process_instance_from_process_model(
process_model=process_model, status="waiting", user=user_one
)
)
process_instance_created_by_user_two_one = (
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_two
)
)
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_two
)
self.create_process_instance_from_process_model(
process_model=process_model, status="waiting", user=user_two
)
human_task_for_user_group_one_one = HumanTaskModel(
process_instance_id=process_instance_created_by_user_one_one.id,
lane_assignment_id=user_group_one.id,
)
human_task_for_user_group_one_two = HumanTaskModel(
process_instance_id=process_instance_created_by_user_one_three.id,
lane_assignment_id=user_group_one.id,
)
human_task_for_user_group_one_three = HumanTaskModel(
process_instance_id=process_instance_created_by_user_two_one.id,
lane_assignment_id=user_group_one.id,
)
human_task_for_user_group_two_one = HumanTaskModel(
process_instance_id=process_instance_created_by_user_two_one.id,
lane_assignment_id=user_group_two.id,
)
human_task_for_user_group_two_two = HumanTaskModel(
process_instance_id=process_instance_created_by_user_one_one.id,
lane_assignment_id=user_group_two.id,
)
db.session.add(human_task_for_user_group_one_one)
db.session.add(human_task_for_user_group_one_two)
db.session.add(human_task_for_user_group_one_three)
db.session.add(human_task_for_user_group_two_one)
db.session.add(human_task_for_user_group_two_two)
db.session.commit()
process_instance_report = ProcessInstanceReportService.report_with_identifier(
user=user_one,
report_identifier="system_report_completed_instances_with_tasks_completed_by_my_groups",
)
report_filter = (
ProcessInstanceReportService.filter_from_metadata_with_overrides(
process_instance_report=process_instance_report,
process_model_identifier=process_model.id,
)
)
response_json = ProcessInstanceReportService.run_process_instance_report(
report_filter=report_filter,
process_instance_report=process_instance_report,
user=user_one,
)
assert len(response_json["results"]) == 2
assert response_json["results"][0]["process_initiator_id"] == user_two.id
assert (
response_json["results"][0]["id"]
== process_instance_created_by_user_two_one.id
)
assert response_json["results"][0]["status"] == "complete"
assert response_json["results"][1]["process_initiator_id"] == user_one.id
assert (
response_json["results"][1]["id"]
== process_instance_created_by_user_one_one.id
)
assert response_json["results"][1]["status"] == "complete"
def test_can_filter_by_with_relation_to_me(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_can_filter_by_with_relation_to_me."""
process_model_id = "runs_without_input/sample"
bpmn_file_location = "sample"
process_model = load_test_spec(
process_model_id,
process_model_source_directory=bpmn_file_location,
)
user_group_one = GroupModel(identifier="group_one")
user_group_two = GroupModel(identifier="group_two")
db.session.add(user_group_one)
db.session.add(user_group_two)
db.session.commit()
user_one = self.find_or_create_user(username="user_one")
user_two = self.find_or_create_user(username="user_two")
user_three = self.find_or_create_user(username="user_three")
UserService.add_user_to_group(user_one, user_group_one)
UserService.add_user_to_group(user_two, user_group_one)
UserService.add_user_to_group(user_three, user_group_two)
# Several processes to ensure they do not return in the result
process_instance_created_by_user_one_one = (
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_one
)
)
process_instance_created_by_user_one_two = (
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_one
)
)
process_instance_created_by_user_one_three = (
self.create_process_instance_from_process_model(
process_model=process_model, status="waiting", user=user_one
)
)
process_instance_created_by_user_two_one = (
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_two
)
)
self.create_process_instance_from_process_model(
process_model=process_model, status="complete", user=user_two
)
self.create_process_instance_from_process_model(
process_model=process_model, status="waiting", user=user_two
)
human_task_for_user_group_one_one = HumanTaskModel(
process_instance_id=process_instance_created_by_user_one_one.id,
lane_assignment_id=user_group_one.id,
)
human_task_for_user_group_one_two = HumanTaskModel(
process_instance_id=process_instance_created_by_user_one_three.id,
lane_assignment_id=user_group_one.id,
)
human_task_for_user_group_one_three = HumanTaskModel(
process_instance_id=process_instance_created_by_user_two_one.id,
lane_assignment_id=user_group_one.id,
)
human_task_for_user_group_two_one = HumanTaskModel(
process_instance_id=process_instance_created_by_user_two_one.id,
lane_assignment_id=user_group_two.id,
)
human_task_for_user_group_two_two = HumanTaskModel(
process_instance_id=process_instance_created_by_user_one_one.id,
lane_assignment_id=user_group_two.id,
)
db.session.add(human_task_for_user_group_one_one)
db.session.add(human_task_for_user_group_one_two)
db.session.add(human_task_for_user_group_one_three)
db.session.add(human_task_for_user_group_two_one)
db.session.add(human_task_for_user_group_two_two)
db.session.commit()
UserService.add_user_to_human_tasks_if_appropriate(user_one)
process_instance_report = ProcessInstanceReportService.report_with_identifier(
user=user_one
)
report_filter = (
ProcessInstanceReportService.filter_from_metadata_with_overrides(
process_instance_report=process_instance_report,
process_model_identifier=process_model.id,
with_relation_to_me=True,
)
)
response_json = ProcessInstanceReportService.run_process_instance_report(
report_filter=report_filter,
process_instance_report=process_instance_report,
user=user_one,
)
assert len(response_json["results"]) == 4
process_instance_ids_in_results = [r["id"] for r in response_json["results"]]
assert (
process_instance_created_by_user_one_one.id
in process_instance_ids_in_results
)
assert (
process_instance_created_by_user_one_two.id
in process_instance_ids_in_results
)
assert (
process_instance_created_by_user_one_three.id
in process_instance_ids_in_results
)
assert (
process_instance_created_by_user_two_one.id
in process_instance_ids_in_results
)

View File

@ -1,4 +1,5 @@
import { modifyProcessIdentifierForPathParam } from '../../src/helpers'; import { modifyProcessIdentifierForPathParam } from '../../src/helpers';
import { miscDisplayName } from '../support/helpers';
describe('process-models', () => { describe('process-models', () => {
beforeEach(() => { beforeEach(() => {
@ -8,7 +9,6 @@ describe('process-models', () => {
cy.logout(); cy.logout();
}); });
const sharedResourceString = 'Shared Resources';
const groupDisplayName = 'Acceptance Tests Group One'; const groupDisplayName = 'Acceptance Tests Group One';
const deleteProcessModelButtonId = 'delete-process-model-button'; const deleteProcessModelButtonId = 'delete-process-model-button';
@ -19,7 +19,7 @@ describe('process-models', () => {
const modelDisplayName = `Test Model 2 ${id}`; const modelDisplayName = `Test Model 2 ${id}`;
const modelId = `test-model-2-${id}`; const modelId = `test-model-2-${id}`;
const newModelDisplayName = `${modelDisplayName} edited`; const newModelDisplayName = `${modelDisplayName} edited`;
cy.contains(sharedResourceString).click(); cy.contains(miscDisplayName).click();
cy.wait(750); cy.wait(750);
cy.contains(groupDisplayName).click(); cy.contains(groupDisplayName).click();
cy.createModel(groupId, modelId, modelDisplayName); cy.createModel(groupId, modelId, modelDisplayName);
@ -39,6 +39,7 @@ describe('process-models', () => {
cy.deleteProcessModelAndConfirm(deleteProcessModelButtonId, groupId); cy.deleteProcessModelAndConfirm(deleteProcessModelButtonId, groupId);
cy.contains(modelId).should('not.exist'); cy.contains(modelId).should('not.exist');
cy.contains(modelDisplayName).should('not.exist');
}); });
it('can create new bpmn, dmn, and json files', () => { it('can create new bpmn, dmn, and json files', () => {
@ -55,12 +56,11 @@ describe('process-models', () => {
const jsonFileName = `json_test_file_${id}`; const jsonFileName = `json_test_file_${id}`;
const decision_acceptance_test_id = `decision_acceptance_test_${id}`; const decision_acceptance_test_id = `decision_acceptance_test_${id}`;
cy.contains(sharedResourceString).click(); cy.contains(miscDisplayName).click();
cy.wait(500); cy.wait(500);
cy.contains(groupDisplayName).click(); cy.contains(groupDisplayName).click();
cy.createModel(groupId, modelId, modelDisplayName); cy.createModel(groupId, modelId, modelDisplayName);
cy.contains(directParentGroupName).click(); cy.contains(groupDisplayName).click();
cy.wait(500);
cy.contains(modelDisplayName).click(); cy.contains(modelDisplayName).click();
cy.url().should( cy.url().should(
'include', 'include',
@ -137,7 +137,7 @@ describe('process-models', () => {
const modelDisplayName = `Test Model 2 ${id}`; const modelDisplayName = `Test Model 2 ${id}`;
const modelId = `test-model-2-${id}`; const modelId = `test-model-2-${id}`;
cy.contains('Add a process group'); cy.contains('Add a process group');
cy.contains(sharedResourceString).click(); cy.contains(miscDisplayName).click();
cy.wait(500); cy.wait(500);
cy.contains(groupDisplayName).click(); cy.contains(groupDisplayName).click();
cy.createModel(groupId, modelId, modelDisplayName); cy.createModel(groupId, modelId, modelDisplayName);
@ -172,7 +172,7 @@ describe('process-models', () => {
.click(); .click();
// in breadcrumb // in breadcrumb
cy.contains(modelId).click(); cy.contains(modelDisplayName).click();
cy.getBySel(deleteProcessModelButtonId).click(); cy.getBySel(deleteProcessModelButtonId).click();
cy.contains('Are you sure'); cy.contains('Are you sure');

View File

@ -1,5 +1,6 @@
import { string } from 'prop-types'; import { string } from 'prop-types';
import { modifyProcessIdentifierForPathParam } from '../../src/helpers'; import { modifyProcessIdentifierForPathParam } from '../../src/helpers';
import { miscDisplayName } from './helpers';
// *********************************************** // ***********************************************
// This example commands.js shows you how to // This example commands.js shows you how to
@ -92,9 +93,9 @@ Cypress.Commands.add(
cy.url().should('include', `/tasks/`); cy.url().should('include', `/tasks/`);
cy.contains('Task: '); cy.contains('Task: ');
} else { } else {
cy.contains(/Process Instance Kicked Off/); cy.contains(/Process Instance.*[kK]icked [oO]ff/);
cy.reload(true); cy.reload(true);
cy.contains(/Process Instance Kicked Off/).should('not.exist'); cy.contains(/Process Instance.*[kK]icked [oO]ff/).should('not.exist');
} }
} }
); );
@ -103,8 +104,8 @@ Cypress.Commands.add(
'navigateToProcessModel', 'navigateToProcessModel',
(groupDisplayName, modelDisplayName, modelIdentifier) => { (groupDisplayName, modelDisplayName, modelIdentifier) => {
cy.navigateToAdmin(); cy.navigateToAdmin();
cy.contains('99-Shared Resources').click(); cy.contains(miscDisplayName).click();
cy.contains(`Process Group: 99-Shared Resources`, { timeout: 10000 }); cy.contains(`Process Group: ${miscDisplayName}`, { timeout: 10000 });
cy.contains(groupDisplayName).click(); cy.contains(groupDisplayName).click();
cy.contains(`Process Group: ${groupDisplayName}`); cy.contains(`Process Group: ${groupDisplayName}`);
// https://stackoverflow.com/q/51254946/6090676 // https://stackoverflow.com/q/51254946/6090676

View File

@ -0,0 +1 @@
export const miscDisplayName = 'Shared Resources';

View File

@ -68,7 +68,7 @@
"@cypress/grep": "^3.1.0", "@cypress/grep": "^3.1.0",
"@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6", "@typescript-eslint/parser": "^5.30.6",
"cypress": "^10.8.0", "cypress": "^12",
"eslint": "^8.19.0", "eslint": "^8.19.0",
"eslint_d": "^12.2.0", "eslint_d": "^12.2.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
@ -9850,9 +9850,9 @@
"integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==" "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A=="
}, },
"node_modules/cypress": { "node_modules/cypress": {
"version": "10.11.0", "version": "12.1.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.11.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.1.0.tgz",
"integrity": "sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA==", "integrity": "sha512-7fz8N84uhN1+ePNDsfQvoWEl4P3/VGKKmAg+bJQFY4onhA37Ys+6oBkGbNdwGeC7n2QqibNVPhk8x3YuQLwzfw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
@ -9903,7 +9903,7 @@
"cypress": "bin/cypress" "cypress": "bin/cypress"
}, },
"engines": { "engines": {
"node": ">=12.0.0" "node": "^14.0.0 || ^16.0.0 || >=18.0.0"
} }
}, },
"node_modules/cypress/node_modules/@types/node": { "node_modules/cypress/node_modules/@types/node": {
@ -38586,9 +38586,9 @@
"integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==" "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A=="
}, },
"cypress": { "cypress": {
"version": "10.11.0", "version": "12.1.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.11.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.1.0.tgz",
"integrity": "sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA==", "integrity": "sha512-7fz8N84uhN1+ePNDsfQvoWEl4P3/VGKKmAg+bJQFY4onhA37Ys+6oBkGbNdwGeC7n2QqibNVPhk8x3YuQLwzfw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@cypress/request": "^2.88.10", "@cypress/request": "^2.88.10",

View File

@ -104,7 +104,7 @@
"@cypress/grep": "^3.1.0", "@cypress/grep": "^3.1.0",
"@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6", "@typescript-eslint/parser": "^5.30.6",
"cypress": "^10.8.0", "cypress": "^12",
"eslint": "^8.19.0", "eslint": "^8.19.0",
"eslint_d": "^12.2.0", "eslint_d": "^12.2.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",

View File

@ -13,6 +13,7 @@ import AdminRoutes from './routes/AdminRoutes';
import { ErrorForDisplay } from './interfaces'; import { ErrorForDisplay } from './interfaces';
import { AbilityContext } from './contexts/Can'; import { AbilityContext } from './contexts/Can';
import UserService from './services/UserService';
export default function App() { export default function App() {
const [errorMessage, setErrorMessage] = useState<ErrorForDisplay | null>( const [errorMessage, setErrorMessage] = useState<ErrorForDisplay | null>(
@ -24,6 +25,11 @@ export default function App() {
[errorMessage] [errorMessage]
); );
if (!UserService.isLoggedIn()) {
UserService.doLogin();
return null;
}
const ability = defineAbility(() => {}); const ability = defineAbility(() => {});
let errorTag = null; let errorTag = null;

View File

@ -0,0 +1,5 @@
export default class ProcessInstanceClass {
static terminalStatuses() {
return ['complete', 'error', 'terminated'];
}
}

View File

@ -8,7 +8,7 @@ export default function MyCompletedInstances() {
filtersEnabled={false} filtersEnabled={false}
paginationQueryParamPrefix={paginationQueryParamPrefix} paginationQueryParamPrefix={paginationQueryParamPrefix}
perPageOptions={[2, 5, 25]} perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_instances_initiated_by_me" reportIdentifier="system_report_completed_instances_initiated_by_me"
showReports={false} showReports={false}
/> />
); );

View File

@ -24,6 +24,7 @@ import UserService from '../services/UserService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces'; import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService'; import { usePermissionFetcher } from '../hooks/PermissionService';
import { UnauthenticatedError } from '../services/HttpService';
// for ref: https://react-bootstrap.github.io/components/navbar/ // for ref: https://react-bootstrap.github.io/components/navbar/
export default function NavigationBar() { export default function NavigationBar() {
@ -39,6 +40,11 @@ export default function NavigationBar() {
const [activeKey, setActiveKey] = useState<string>(''); const [activeKey, setActiveKey] = useState<string>('');
const { targetUris } = useUriListForPermissions(); const { targetUris } = useUriListForPermissions();
// App.jsx forces login (which redirects to keycloak) so we should never get here if we're not logged in.
if (!UserService.isLoggedIn()) {
throw new UnauthenticatedError('You must be authenticated to do this.');
}
const permissionRequestData: PermissionsToCheck = { const permissionRequestData: PermissionsToCheck = {
[targetUris.authenticationListPath]: ['GET'], [targetUris.authenticationListPath]: ['GET'],
[targetUris.messageInstanceListPath]: ['GET'], [targetUris.messageInstanceListPath]: ['GET'],
@ -135,6 +141,9 @@ export default function NavigationBar() {
}; };
const headerMenuItems = () => { const headerMenuItems = () => {
if (!UserService.isLoggedIn()) {
return null;
}
return ( return (
<> <>
<HeaderMenuItem href="/" isCurrentPage={isActivePage('/')}> <HeaderMenuItem href="/" isCurrentPage={isActivePage('/')}>

View File

@ -79,6 +79,8 @@ type OwnProps = {
textToShowIfEmpty?: string; textToShowIfEmpty?: string;
paginationClassName?: string; paginationClassName?: string;
autoReload?: boolean; autoReload?: boolean;
additionalParams?: string;
variant?: string;
}; };
interface dateParameters { interface dateParameters {
@ -90,12 +92,18 @@ export default function ProcessInstanceListTable({
processModelFullIdentifier, processModelFullIdentifier,
paginationQueryParamPrefix, paginationQueryParamPrefix,
perPageOptions, perPageOptions,
additionalParams,
showReports = true, showReports = true,
reportIdentifier, reportIdentifier,
textToShowIfEmpty, textToShowIfEmpty,
paginationClassName, paginationClassName,
autoReload = false, autoReload = false,
variant = 'for-me',
}: OwnProps) { }: OwnProps) {
let apiPath = '/process-instances/for-me';
if (variant === 'all') {
apiPath = '/process-instances';
}
const params = useParams(); const params = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const navigate = useNavigate(); const navigate = useNavigate();
@ -124,6 +132,11 @@ export default function ProcessInstanceListTable({
const setErrorMessage = (useContext as any)(ErrorContext)[1]; const setErrorMessage = (useContext as any)(ErrorContext)[1];
const processInstancePathPrefix =
variant === 'all'
? '/admin/process-instances'
: '/admin/process-instances/for-me';
const [processStatusAllOptions, setProcessStatusAllOptions] = useState<any[]>( const [processStatusAllOptions, setProcessStatusAllOptions] = useState<any[]>(
[] []
); );
@ -253,8 +266,12 @@ export default function ProcessInstanceListTable({
} }
); );
if (additionalParams) {
queryParamString += `&${additionalParams}`;
}
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/process-instances?${queryParamString}`, path: `${apiPath}?${queryParamString}`,
successCallback: setProcessInstancesFromResult, successCallback: setProcessInstancesFromResult,
}); });
} }
@ -320,6 +337,8 @@ export default function ProcessInstanceListTable({
processModelFullIdentifier, processModelFullIdentifier,
perPageOptions, perPageOptions,
reportIdentifier, reportIdentifier,
additionalParams,
apiPath,
]); ]);
// This sets the filter data using the saved reports returned from the initial instance_list query. // This sets the filter data using the saved reports returned from the initial instance_list query.
@ -509,7 +528,7 @@ export default function ProcessInstanceListTable({
setErrorMessage(null); setErrorMessage(null);
setProcessInstanceReportJustSaved(null); setProcessInstanceReportJustSaved(null);
navigate(`/admin/process-instances?${queryParamString}`); navigate(`${processInstancePathPrefix}?${queryParamString}`);
}; };
const dateComponent = ( const dateComponent = (
@ -608,7 +627,7 @@ export default function ProcessInstanceListTable({
setErrorMessage(null); setErrorMessage(null);
setProcessInstanceReportJustSaved(mode || null); setProcessInstanceReportJustSaved(mode || null);
navigate(`/admin/process-instances${queryParamString}`); navigate(`${processInstancePathPrefix}${queryParamString}`);
}; };
const reportColumns = () => { const reportColumns = () => {
@ -843,8 +862,8 @@ export default function ProcessInstanceListTable({
return null; return null;
}} }}
shouldFilterItem={shouldFilterReportColumn} shouldFilterItem={shouldFilterReportColumn}
placeholder="Choose a report column" placeholder="Choose a column to show"
titleText="Report Column" titleText="Column"
/> />
); );
} }
@ -893,7 +912,7 @@ export default function ProcessInstanceListTable({
kind="ghost" kind="ghost"
size="sm" size="sm"
className={`button-tag-icon ${tagTypeClass}`} className={`button-tag-icon ${tagTypeClass}`}
title={`Edit ${reportColumnForEditing.accessor}`} title={`Edit ${reportColumnForEditing.accessor} column`}
onClick={() => { onClick={() => {
setReportColumnToOperateOn(reportColumnForEditing); setReportColumnToOperateOn(reportColumnForEditing);
setShowReportColumnForm(true); setShowReportColumnForm(true);
@ -921,7 +940,7 @@ export default function ProcessInstanceListTable({
<Button <Button
data-qa="add-column-button" data-qa="add-column-button"
renderIcon={AddAlt} renderIcon={AddAlt}
iconDescription="Filter Options" iconDescription="Column options"
className="with-tiny-top-margin" className="with-tiny-top-margin"
kind="ghost" kind="ghost"
hasIconOnly hasIconOnly
@ -1074,7 +1093,7 @@ export default function ProcessInstanceListTable({
return ( return (
<Link <Link
data-qa="process-instance-show-link" data-qa="process-instance-show-link"
to={`/admin/process-instances/${modifiedProcessModelId}/${id}`} to={`${processInstancePathPrefix}/${modifiedProcessModelId}/${id}`}
title={`View process instance ${id}`} title={`View process instance ${id}`}
> >
{id} {id}

View File

@ -35,7 +35,7 @@ export default function ProcessModelListTiles({
setProcessModels(result.results); setProcessModels(result.results);
}; };
// only allow 10 for now until we get the backend only returning certain models for user execution // only allow 10 for now until we get the backend only returning certain models for user execution
let queryParams = '?per_page=20'; let queryParams = '?per_page=1000';
if (processGroup) { if (processGroup) {
queryParams = `${queryParams}&process_group_identifier=${processGroup.id}`; queryParams = `${queryParams}&process_group_identifier=${processGroup.id}`;
} else { } else {

View File

@ -52,11 +52,13 @@ import TouchModule from 'diagram-js/lib/navigation/touch';
// @ts-expect-error TS(7016) FIXME // @ts-expect-error TS(7016) FIXME
import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll'; import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll';
import { useNavigate } from 'react-router-dom';
import { Can } from '@casl/react'; import { Can } from '@casl/react';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import ButtonWithConfirmation from './ButtonWithConfirmation'; import ButtonWithConfirmation from './ButtonWithConfirmation';
import { makeid } from '../helpers'; import { getBpmnProcessIdentifiers, makeid } from '../helpers';
import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck, ProcessInstanceTask } from '../interfaces'; import { PermissionsToCheck, ProcessInstanceTask } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService'; import { usePermissionFetcher } from '../hooks/PermissionService';
@ -68,6 +70,7 @@ type OwnProps = {
completedProcessInstanceTasks?: ProcessInstanceTask[] | null; completedProcessInstanceTasks?: ProcessInstanceTask[] | null;
saveDiagram?: (..._args: any[]) => any; saveDiagram?: (..._args: any[]) => any;
onDeleteFile?: (..._args: any[]) => any; onDeleteFile?: (..._args: any[]) => any;
isPrimaryFile?: boolean;
onSetPrimaryFile?: (..._args: any[]) => any; onSetPrimaryFile?: (..._args: any[]) => any;
diagramXML?: string | null; diagramXML?: string | null;
fileName?: string; fileName?: string;
@ -92,6 +95,7 @@ export default function ReactDiagramEditor({
completedProcessInstanceTasks, completedProcessInstanceTasks,
saveDiagram, saveDiagram,
onDeleteFile, onDeleteFile,
isPrimaryFile,
onSetPrimaryFile, onSetPrimaryFile,
diagramXML, diagramXML,
fileName, fileName,
@ -119,6 +123,7 @@ export default function ReactDiagramEditor({
[targetUris.processModelFileShowPath]: ['POST', 'GET', 'PUT', 'DELETE'], [targetUris.processModelFileShowPath]: ['POST', 'GET', 'PUT', 'DELETE'],
}; };
const { ability } = usePermissionFetcher(permissionRequestData); const { ability } = usePermissionFetcher(permissionRequestData);
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
if (diagramModelerState) { if (diagramModelerState) {
@ -228,8 +233,10 @@ export default function ReactDiagramEditor({
function handleElementClick(event: any) { function handleElementClick(event: any) {
if (onElementClick) { if (onElementClick) {
const canvas = diagramModeler.get('canvas'); const canvas = diagramModeler.get('canvas');
const rootElement = canvas.getRootElement(); const bpmnProcessIdentifiers = getBpmnProcessIdentifiers(
onElementClick(event.element, rootElement); canvas.getRootElement()
);
onElementClick(event.element, bpmnProcessIdentifiers);
} }
} }
@ -354,11 +361,15 @@ export default function ReactDiagramEditor({
canvas: any, canvas: any,
processInstanceTask: ProcessInstanceTask, processInstanceTask: ProcessInstanceTask,
bpmnIoClassName: string, bpmnIoClassName: string,
bpmnRootElementId: string bpmnProcessIdentifiers: string[]
) { ) {
if (checkTaskCanBeHighlighted(processInstanceTask.name)) { if (checkTaskCanBeHighlighted(processInstanceTask.name)) {
try { try {
if (bpmnRootElementId === processInstanceTask.process_identifier) { if (
bpmnProcessIdentifiers.includes(
processInstanceTask.process_identifier
)
) {
canvas.addMarker(processInstanceTask.name, bpmnIoClassName); canvas.addMarker(processInstanceTask.name, bpmnIoClassName);
} }
} catch (bpmnIoError: any) { } catch (bpmnIoError: any) {
@ -400,24 +411,28 @@ export default function ReactDiagramEditor({
// Option 3 at: // Option 3 at:
// https://github.com/bpmn-io/bpmn-js-examples/tree/master/colors // https://github.com/bpmn-io/bpmn-js-examples/tree/master/colors
if (readyOrWaitingProcessInstanceTasks) { if (readyOrWaitingProcessInstanceTasks) {
const rootElement = canvas.getRootElement(); const bpmnProcessIdentifiers = getBpmnProcessIdentifiers(
canvas.getRootElement()
);
readyOrWaitingProcessInstanceTasks.forEach((readyOrWaitingBpmnTask) => { readyOrWaitingProcessInstanceTasks.forEach((readyOrWaitingBpmnTask) => {
highlightBpmnIoElement( highlightBpmnIoElement(
canvas, canvas,
readyOrWaitingBpmnTask, readyOrWaitingBpmnTask,
'active-task-highlight', 'active-task-highlight',
rootElement.id bpmnProcessIdentifiers
); );
}); });
} }
if (completedProcessInstanceTasks) { if (completedProcessInstanceTasks) {
const rootElement = canvas.getRootElement(); const bpmnProcessIdentifiers = getBpmnProcessIdentifiers(
canvas.getRootElement()
);
completedProcessInstanceTasks.forEach((completedTask) => { completedProcessInstanceTasks.forEach((completedTask) => {
highlightBpmnIoElement( highlightBpmnIoElement(
canvas, canvas,
completedTask, completedTask,
'completed-task-highlight', 'completed-task-highlight',
rootElement.id bpmnProcessIdentifiers
); );
}); });
} }
@ -542,6 +557,8 @@ export default function ReactDiagramEditor({
}); });
}; };
const canViewXml = fileName !== undefined;
const userActionOptions = () => { const userActionOptions = () => {
if (diagramType !== 'readonly') { if (diagramType !== 'readonly') {
return ( return (
@ -558,7 +575,7 @@ export default function ReactDiagramEditor({
a={targetUris.processModelFileShowPath} a={targetUris.processModelFileShowPath}
ability={ability} ability={ability}
> >
{fileName && ( {fileName && !isPrimaryFile && (
<ButtonWithConfirmation <ButtonWithConfirmation
description={`Delete file ${fileName}?`} description={`Delete file ${fileName}?`}
onConfirmation={handleDelete} onConfirmation={handleDelete}
@ -580,6 +597,23 @@ export default function ReactDiagramEditor({
> >
<Button onClick={downloadXmlFile}>Download</Button> <Button onClick={downloadXmlFile}>Download</Button>
</Can> </Can>
<Can
I="GET"
a={targetUris.processModelFileShowPath}
ability={ability}
>
{canViewXml && (
<Button
onClick={() => {
navigate(
`/admin/process-models/${processModelId}/form/${fileName}`
);
}}
>
View XML
</Button>
)}
</Can>
</> </>
); );
} }

View File

@ -0,0 +1,203 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Button, Table } from '@carbon/react';
import { Link, useSearchParams } from 'react-router-dom';
import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessIdentifierForPathParam,
refreshAtInterval,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject, ProcessInstanceTask } from '../interfaces';
import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600;
type OwnProps = {
apiPath: string;
tableTitle: string;
tableDescription: string;
additionalParams?: string;
paginationQueryParamPrefix?: string;
paginationClassName?: string;
autoReload?: boolean;
showStartedBy?: boolean;
showWaitingOn?: boolean;
textToShowIfEmpty?: string;
};
export default function TaskListTable({
apiPath,
tableTitle,
tableDescription,
additionalParams,
paginationQueryParamPrefix,
paginationClassName,
textToShowIfEmpty,
autoReload = false,
showStartedBy = true,
showWaitingOn = true,
}: OwnProps) {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState<ProcessInstanceTask[] | null>(null);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
useEffect(() => {
const getTasks = () => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
};
let params = `?per_page=${perPage}&page=${page}`;
if (additionalParams) {
params += `&${additionalParams}`;
}
HttpService.makeCallToBackend({
path: `${apiPath}${params}`,
successCallback: setTasksFromResult,
});
};
getTasks();
if (autoReload) {
return refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
}
return undefined;
}, [
searchParams,
additionalParams,
apiPath,
paginationQueryParamPrefix,
autoReload,
]);
const buildTable = () => {
if (!tasks) {
return null;
}
const rows = tasks.map((row) => {
const rowToUse = row as any;
const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`;
const modifiedProcessModelIdentifier =
modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier);
return (
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-instances/${modifiedProcessModelIdentifier}/${rowToUse.process_instance_id}`}
title={`View process instance ${rowToUse.process_instance_id}`}
>
{rowToUse.process_instance_id}
</Link>
</td>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
title={rowToUse.process_model_identifier}
>
{rowToUse.process_model_display_name}
</Link>
</td>
<td
title={`task id: ${rowToUse.name}, spiffworkflow task guid: ${rowToUse.id}`}
>
{rowToUse.task_title}
</td>
{showStartedBy ? <td>{rowToUse.username}</td> : ''}
{showWaitingOn ? <td>{rowToUse.group_identifier || '-'}</td> : ''}
<td>
{convertSecondsToFormattedDateTime(
rowToUse.created_at_in_seconds
) || '-'}
</td>
<TableCellWithTimeAgoInWords
timeInSeconds={rowToUse.updated_at_in_seconds}
/>
<td>
<Button
variant="primary"
href={taskUrl}
hidden={rowToUse.process_instance_status === 'suspended'}
disabled={!rowToUse.current_user_is_potential_owner}
>
Go
</Button>
</td>
</tr>
);
});
let tableHeaders = ['Id', 'Process', 'Task'];
if (showStartedBy) {
tableHeaders.push('Started By');
}
if (showWaitingOn) {
tableHeaders.push('Waiting For');
}
tableHeaders = tableHeaders.concat([
'Date Started',
'Last Updated',
'Actions',
]);
return (
<Table striped bordered>
<thead>
<tr>
{tableHeaders.map((tableHeader: string) => {
return <th>{tableHeader}</th>;
})}
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
};
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return (
<p className="no-results-message with-large-bottom-margin">
{textToShowIfEmpty}
</p>
);
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
return (
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
paginationClassName={paginationClassName}
/>
);
};
if (tasks) {
return (
<>
<h2>{tableTitle}</h2>
<p className="data-table-description">{tableDescription}</p>
{tasksComponent()}
</>
);
}
return null;
}

View File

@ -1,156 +1,18 @@
import { useEffect, useState } from 'react'; import TaskListTable from './TaskListTable';
// @ts-ignore
import { Button, Table } from '@carbon/react';
import { Link, useSearchParams } from 'react-router-dom';
import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessIdentifierForPathParam,
refreshAtInterval,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const paginationQueryParamPrefix = 'tasks_for_my_open_processes'; const paginationQueryParamPrefix = 'tasks_for_my_open_processes';
const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600;
export default function MyOpenProcesses() { export default function MyOpenProcesses() {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
useEffect(() => {
const getTasks = () => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
};
HttpService.makeCallToBackend({
path: `/tasks/for-my-open-processes?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
};
getTasks();
return refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
}, [searchParams]);
const buildTable = () => {
const rows = tasks.map((row) => {
const rowToUse = row as any;
const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`;
const modifiedProcessModelIdentifier =
modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier);
return ( return (
<tr key={rowToUse.id}> <TaskListTable
<td> apiPath="/tasks/for-my-open-processes"
<Link
data-qa="process-instance-show-link"
to={`/admin/process-instances/${modifiedProcessModelIdentifier}/${rowToUse.process_instance_id}`}
title={`View process instance ${rowToUse.process_instance_id}`}
>
{rowToUse.process_instance_id}
</Link>
</td>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
title={rowToUse.process_model_identifier}
>
{rowToUse.process_model_display_name}
</Link>
</td>
<td
title={`task id: ${rowToUse.name}, spiffworkflow task guid: ${rowToUse.id}`}
>
{rowToUse.task_title}
</td>
<td>{rowToUse.group_identifier || '-'}</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.created_at_in_seconds
) || '-'}
</td>
<TableCellWithTimeAgoInWords
timeInSeconds={rowToUse.updated_at_in_seconds}
/>
<td>
<Button
variant="primary"
href={taskUrl}
hidden={rowToUse.process_instance_status === 'suspended'}
disabled={!rowToUse.current_user_is_potential_owner}
>
Go
</Button>
</td>
</tr>
);
});
return (
<Table striped bordered>
<thead>
<tr>
<th>Id</th>
<th>Process</th>
<th>Task</th>
<th>Waiting For</th>
<th>Date Started</th>
<th>Last Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
};
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return (
<p className="no-results-message with-large-bottom-margin">
There are no tasks for processes you started at this time.
</p>
);
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
return (
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix} paginationQueryParamPrefix={paginationQueryParamPrefix}
tableTitle="Tasks for my open instances"
tableDescription="These tasks are for processes you started which are not complete. You may not have an action to take at this time. See below for tasks waiting on you."
paginationClassName="with-large-bottom-margin" paginationClassName="with-large-bottom-margin"
textToShowIfEmpty="There are no tasks for processes you started at this time."
autoReload
showStartedBy={false}
/> />
); );
};
return (
<>
<h2>My open instances</h2>
<p className="data-table-description">
These tasks are for processes you started which are not complete. You
may not have an action to take at this time. See below for tasks waiting
on you.
</p>
{tasksComponent()}
</>
);
} }

View File

@ -1,149 +1,16 @@
import { useEffect, useState } from 'react'; import TaskListTable from './TaskListTable';
// @ts-ignore
import { Button, Table } from '@carbon/react';
import { Link, useSearchParams } from 'react-router-dom';
import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessIdentifierForPathParam,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
export default function TasksWaitingForMe() { export default function TasksWaitingForMe() {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
'tasks_waiting_for_me'
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
};
HttpService.makeCallToBackend({
path: `/tasks/for-me?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
}, [searchParams]);
const buildTable = () => {
const rows = tasks.map((row) => {
const rowToUse = row as any;
const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`;
const modifiedProcessModelIdentifier =
modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier);
return ( return (
<tr key={rowToUse.id}> <TaskListTable
<td> apiPath="/tasks/for-me"
<Link
data-qa="process-instance-show-link"
to={`/admin/${modifiedProcessModelIdentifier}/${rowToUse.process_instance_id}`}
title={`View process instance ${rowToUse.process_instance_id}`}
>
{rowToUse.process_instance_id}
</Link>
</td>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
title={rowToUse.process_model_identifier}
>
{rowToUse.process_model_display_name}
</Link>
</td>
<td
title={`task id: ${rowToUse.name}, spiffworkflow task guid: ${rowToUse.id}`}
>
{rowToUse.task_title}
</td>
<td>{rowToUse.username}</td>
<td>{rowToUse.group_identifier || '-'}</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.created_at_in_seconds
) || '-'}
</td>
<TableCellWithTimeAgoInWords
timeInSeconds={rowToUse.updated_at_in_seconds}
/>
<td>
<Button
variant="primary"
href={taskUrl}
hidden={rowToUse.process_instance_status === 'suspended'}
disabled={!rowToUse.current_user_is_potential_owner}
>
Go
</Button>
</td>
</tr>
);
});
return (
<Table striped bordered>
<thead>
<tr>
<th>Id</th>
<th>Process</th>
<th>Task</th>
<th>Started By</th>
<th>Waiting For</th>
<th>Date Started</th>
<th>Last Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
};
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return (
<p className="no-results-message with-large-bottom-margin">
You have no task assignments at this time.
</p>
);
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
'tasks_waiting_for_me'
);
return (
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix="tasks_waiting_for_me" paginationQueryParamPrefix="tasks_waiting_for_me"
tableTitle="Tasks waiting for me"
tableDescription="These processes are waiting on you to complete the next task. All are processes created by others that are now actionable by you."
paginationClassName="with-large-bottom-margin" paginationClassName="with-large-bottom-margin"
textToShowIfEmpty="No tasks are waiting for you."
autoReload
showWaitingOn={false}
/> />
); );
};
return (
<>
<h2>Tasks waiting for me</h2>
<p className="data-table-description">
These processes are waiting on you to complete the next task. All are
processes created by others that are now actionable by you.
</p>
{tasksComponent()}
</>
);
} }

View File

@ -1,156 +1,41 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
// @ts-ignore
import { Button, Table } from '@carbon/react';
import { Link, useSearchParams } from 'react-router-dom';
import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessIdentifierForPathParam,
refreshAtInterval,
} from '../helpers';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces'; import TaskListTable from './TaskListTable';
import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const paginationQueryParamPrefix = 'tasks_waiting_for_my_groups';
const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600;
export default function TasksWaitingForMyGroups() { export default function TasksWaitingForMyGroups() {
const [searchParams] = useSearchParams(); const [userGroups, setUserGroups] = useState<string[] | null>(null);
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
useEffect(() => { useEffect(() => {
const getTasks = () => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
};
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/tasks/for-my-groups?per_page=${perPage}&page=${page}`, path: `/user-groups/for-current-user`,
successCallback: setTasksFromResult, successCallback: setUserGroups,
}); });
}; }, [setUserGroups]);
getTasks();
return refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
}, [searchParams]);
const buildTable = () => { const tableComponents = () => {
const rows = tasks.map((row) => { if (!userGroups) {
const rowToUse = row as any; return null;
const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`;
const modifiedProcessModelIdentifier =
modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier);
return (
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-instances/${modifiedProcessModelIdentifier}/${rowToUse.process_instance_id}`}
title={`View process instance ${rowToUse.process_instance_id}`}
>
{rowToUse.process_instance_id}
</Link>
</td>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
title={rowToUse.process_model_identifier}
>
{rowToUse.process_model_display_name}
</Link>
</td>
<td
title={`task id: ${rowToUse.name}, spiffworkflow task guid: ${rowToUse.id}`}
>
{rowToUse.task_title}
</td>
<td>{rowToUse.username}</td>
<td>{rowToUse.group_identifier || '-'}</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.created_at_in_seconds
) || '-'}
</td>
<TableCellWithTimeAgoInWords
timeInSeconds={rowToUse.updated_at_in_seconds}
/>
<td>
<Button
variant="primary"
href={taskUrl}
hidden={rowToUse.process_instance_status === 'suspended'}
disabled={!rowToUse.current_user_is_potential_owner}
>
Go
</Button>
</td>
</tr>
);
});
return (
<Table striped bordered>
<thead>
<tr>
<th>Id</th>
<th>Process</th>
<th>Task</th>
<th>Started By</th>
<th>Waiting For</th>
<th>Date Started</th>
<th>Last Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
};
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return (
<p className="no-results-message">
Your groups have no task assignments at this time.
</p>
);
} }
const { page, perPage } = getPageInfoFromSearchParams(
searchParams, return userGroups.map((userGroup: string) => {
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
return ( return (
<PaginationForTable <TaskListTable
page={page} apiPath="/tasks/for-my-groups"
perPage={perPage} additionalParams={`user_group_identifier=${userGroup}`}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]} paginationQueryParamPrefix={`group-tasks-${userGroup}`}
pagination={pagination} tableTitle={`Tasks waiting for group: ${userGroup}`}
tableToDisplay={buildTable()} tableDescription={`This is a list of tasks for the ${userGroup} group. They can be completed by any member of the group.`}
paginationQueryParamPrefix={paginationQueryParamPrefix} paginationClassName="with-large-bottom-margin"
textToShowIfEmpty="This group has no task assignments at this time."
autoReload
showWaitingOn={false}
/> />
); );
});
}; };
return ( if (userGroups) {
<> return <>{tableComponents()}</>;
<h2>Tasks waiting for my groups</h2> }
<p className="data-table-description"> return null;
This is a list of tasks for groups you belong to that can be completed
by any member of the group.
</p>
{tasksComponent()}
</>
);
} }

View File

@ -213,3 +213,24 @@ export const refreshAtInterval = (
clearTimeout(timeoutRef); clearTimeout(timeoutRef);
}; };
}; };
const getChildProcesses = (bpmnElement: any) => {
let elements: string[] = [];
bpmnElement.children.forEach((c: any) => {
if (c.type === 'bpmn:Participant') {
if (c.businessObject.processRef) {
elements.push(c.businessObject.processRef.id);
}
elements = [...elements, ...getChildProcesses(c)];
} else if (c.type === 'bpmn:SubProcess') {
elements.push(c.id);
}
});
return elements;
};
export const getBpmnProcessIdentifiers = (rootBpmnElement: any) => {
const childProcesses = getChildProcesses(rootBpmnElement);
childProcesses.push(rootBpmnElement.businessObject.id);
return childProcesses;
};

View File

@ -9,12 +9,17 @@ export const useUriListForPermissions = () => {
messageInstanceListPath: '/v1.0/messages', messageInstanceListPath: '/v1.0/messages',
processGroupListPath: '/v1.0/process-groups', processGroupListPath: '/v1.0/process-groups',
processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`, 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}`, processInstanceActionPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}`,
processInstanceCreatePath: `/v1.0/process-instances/${params.process_model_id}`,
processInstanceListPath: '/v1.0/process-instances', processInstanceListPath: '/v1.0/process-instances',
processInstanceLogListPath: `/v1.0/logs/${params.process_model_id}/${params.process_instance_id}`, processInstanceLogListPath: `/v1.0/logs/${params.process_model_id}/${params.process_instance_id}`,
processInstanceReportListPath: '/v1.0/process-instances/reports', processInstanceReportListPath: '/v1.0/process-instances/reports',
processInstanceTaskListPath: `/v1.0/task-data/${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}`,
processInstanceTaskListDataPath: `/v1.0/task-data/${params.process_model_id}/${params.process_instance_id}`,
processInstanceTaskListPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}/task-info`,
processInstanceTaskListForMePath: `/v1.0/process-instances/for-me/${params.process_model_id}/${params.process_instance_id}/task-info`,
processInstanceTerminatePath: `/v1.0/process-instance-terminate/${params.process_model_id}/${params.process_instance_id}`,
processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`, processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`,
processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`, processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`,
processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`, processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`,

View File

@ -13,6 +13,12 @@ export interface RecentProcessModel {
export interface ProcessInstanceTask { export interface ProcessInstanceTask {
id: string; id: string;
process_model_display_name: string;
process_model_identifier: string;
task_title: string;
lane_assignment_id: string;
process_instance_status: number;
updated_at_in_seconds: number;
state: string; state: string;
process_identifier: string; process_identifier: string;
name: string; name: string;
@ -46,6 +52,10 @@ export interface ProcessInstance {
id: number; id: number;
process_model_identifier: string; process_model_identifier: string;
process_model_display_name: string; process_model_display_name: string;
status: string;
start_in_seconds: number | null;
end_in_seconds: number | null;
bpmn_xml_file_contents?: string;
spiff_step?: number; spiff_step?: number;
} }

View File

@ -62,21 +62,25 @@ export default function AdminRoutes() {
path="process-models/:process_model_id/files/:file_name" path="process-models/:process_model_id/files/:file_name"
element={<ProcessModelEditDiagram />} element={<ProcessModelEditDiagram />}
/> />
<Route
path="process-models/:process_model_id/process-instances"
element={<ProcessInstanceList />}
/>
<Route <Route
path="process-models/:process_model_id/edit" path="process-models/:process_model_id/edit"
element={<ProcessModelEdit />} element={<ProcessModelEdit />}
/> />
<Route
path="process-instances/for-me/:process_model_id/:process_instance_id"
element={<ProcessInstanceShow variant="for-me" />}
/>
<Route
path="process-instances/for-me/:process_model_id/:process_instance_id/:spiff_step"
element={<ProcessInstanceShow variant="for-me" />}
/>
<Route <Route
path="process-instances/:process_model_id/:process_instance_id" path="process-instances/:process_model_id/:process_instance_id"
element={<ProcessInstanceShow />} element={<ProcessInstanceShow variant="all" />}
/> />
<Route <Route
path="process-instances/:process_model_id/:process_instance_id/:spiff_step" path="process-instances/:process_model_id/:process_instance_id/:spiff_step"
element={<ProcessInstanceShow />} element={<ProcessInstanceShow variant="all" />}
/> />
<Route <Route
path="process-instances/reports" path="process-instances/reports"
@ -106,7 +110,18 @@ export default function AdminRoutes() {
path="logs/:process_model_id/:process_instance_id" path="logs/:process_model_id/:process_instance_id"
element={<ProcessInstanceLogList />} element={<ProcessInstanceLogList />}
/> />
<Route path="process-instances" element={<ProcessInstanceList />} /> <Route
path="process-instances"
element={<ProcessInstanceList variant="for-me" />}
/>
<Route
path="process-instances/for-me"
element={<ProcessInstanceList variant="for-me" />}
/>
<Route
path="process-instances/all"
element={<ProcessInstanceList variant="all" />}
/>
<Route path="messages" element={<MessageInstanceList />} /> <Route path="messages" element={<MessageInstanceList />} />
<Route path="configuration/*" element={<Configuration />} /> <Route path="configuration/*" element={<Configuration />} />
<Route <Route

View File

@ -1,6 +1,45 @@
import { useEffect, useState } from 'react';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable'; import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import HttpService from '../services/HttpService';
export default function CompletedInstances() { export default function CompletedInstances() {
const [userGroups, setUserGroups] = useState<string[] | null>(null);
useEffect(() => {
HttpService.makeCallToBackend({
path: `/user-groups/for-current-user`,
successCallback: setUserGroups,
});
}, [setUserGroups]);
const groupTableComponents = () => {
if (!userGroups) {
return null;
}
return userGroups.map((userGroup: string) => {
return (
<>
<h2>With tasks completed by group: {userGroup}</h2>
<p className="data-table-description">
This is a list of instances with tasks that were completed by the{' '}
{userGroup} group.
</p>
<ProcessInstanceListTable
filtersEnabled={false}
paginationQueryParamPrefix="group_completed_instances"
paginationClassName="with-large-bottom-margin"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_completed_instances_with_tasks_completed_by_my_groups"
showReports={false}
textToShowIfEmpty="This group has no completed instances at this time."
additionalParams={`user_group_identifier=${userGroup}`}
/>
</>
);
});
};
return ( return (
<> <>
<h2>My completed instances</h2> <h2>My completed instances</h2>
@ -11,13 +50,13 @@ export default function CompletedInstances() {
filtersEnabled={false} filtersEnabled={false}
paginationQueryParamPrefix="my_completed_instances" paginationQueryParamPrefix="my_completed_instances"
perPageOptions={[2, 5, 25]} perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_instances_initiated_by_me" reportIdentifier="system_report_completed_instances_initiated_by_me"
showReports={false} showReports={false}
textToShowIfEmpty="You have no completed instances at this time." textToShowIfEmpty="You have no completed instances at this time."
paginationClassName="with-large-bottom-margin" paginationClassName="with-large-bottom-margin"
autoReload autoReload
/> />
<h2>Tasks completed by me</h2> <h2>With tasks completed by me</h2>
<p className="data-table-description"> <p className="data-table-description">
This is a list of instances where you have completed tasks. This is a list of instances where you have completed tasks.
</p> </p>
@ -25,24 +64,12 @@ export default function CompletedInstances() {
filtersEnabled={false} filtersEnabled={false}
paginationQueryParamPrefix="my_completed_tasks" paginationQueryParamPrefix="my_completed_tasks"
perPageOptions={[2, 5, 25]} perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_instances_with_tasks_completed_by_me" reportIdentifier="system_report_completed_instances_with_tasks_completed_by_me"
showReports={false} showReports={false}
textToShowIfEmpty="You have no completed tasks at this time." textToShowIfEmpty="You have no completed instances at this time."
paginationClassName="with-large-bottom-margin" paginationClassName="with-large-bottom-margin"
/> />
<h2>Tasks completed by my groups</h2> {groupTableComponents()}
<p className="data-table-description">
This is a list of instances with tasks that were completed by groups you
belong to.
</p>
<ProcessInstanceListTable
filtersEnabled={false}
paginationQueryParamPrefix="group_completed_tasks"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_instances_with_tasks_completed_by_my_groups"
showReports={false}
textToShowIfEmpty="Your group has no completed tasks at this time."
/>
</> </>
); );
} }

View File

@ -1,15 +1,33 @@
import { useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
import 'react-bootstrap-typeahead/css/Typeahead.css'; import 'react-bootstrap-typeahead/css/Typeahead.css';
import 'react-bootstrap-typeahead/css/Typeahead.bs5.css'; import 'react-bootstrap-typeahead/css/Typeahead.bs5.css';
// @ts-ignore
import { Tabs, TabList, Tab } from '@carbon/react';
import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable'; import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import {getProcessModelFullIdentifierFromSearchParams, modifyProcessIdentifierForPathParam} from '../helpers'; import {getProcessModelFullIdentifierFromSearchParams, modifyProcessIdentifierForPathParam} from '../helpers';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
export default function ProcessInstanceList() { type OwnProps = {
variant: string;
};
export default function ProcessInstanceList({ variant }: OwnProps) {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processInstanceListPath]: ['GET'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
const processInstanceBreadcrumbElement = () => { const processInstanceBreadcrumbElement = () => {
const processModelFullIdentifier = const processModelFullIdentifier =
getProcessModelFullIdentifierFromSearchParams(searchParams); getProcessModelFullIdentifierFromSearchParams(searchParams);
@ -24,10 +42,11 @@ export default function ProcessInstanceList() {
<ProcessBreadcrumb <ProcessBreadcrumb
hotCrumbs={[ hotCrumbs={[
['Process Groups', '/admin'], ['Process Groups', '/admin'],
[ {
`Process Model: ${processModelFullIdentifier}`, entityToExplode: processModelFullIdentifier,
`process-models/${modifiedProcessModelId}`, entityType: 'process-model-id',
], linkLastItem: true,
},
['Process Instances'], ['Process Instances'],
]} ]}
/> />
@ -35,13 +54,44 @@ export default function ProcessInstanceList() {
}; };
const processInstanceTitleElement = () => { const processInstanceTitleElement = () => {
return <h1>Process Instances</h1>; if (variant === 'all') {
return <h1>All Process Instances</h1>;
}
return <h1>My Process Instances</h1>;
}; };
let selectedTabIndex = 0;
if (variant === 'all') {
selectedTabIndex = 1;
}
return ( return (
<> <>
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">
<Tab
title="Only show process instances for the current user."
onClick={() => {
navigate('/admin/process-instances/for-me');
}}
>
For Me
</Tab>
<Can I="GET" a={targetUris.processInstanceListPath} ability={ability}>
<Tab
title="Show all process instances for all users."
onClick={() => {
navigate('/admin/process-instances/all');
}}
>
All
</Tab>
</Can>
</TabList>
</Tabs>
<br />
{processInstanceBreadcrumbElement()} {processInstanceBreadcrumbElement()}
{processInstanceTitleElement()} {processInstanceTitleElement()}
<ProcessInstanceListTable /> <ProcessInstanceListTable variant={variant} />
</> </>
); );
} }

View File

@ -45,8 +45,13 @@ import {
ProcessInstanceTask, ProcessInstanceTask,
} from '../interfaces'; } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService'; import { usePermissionFetcher } from '../hooks/PermissionService';
import ProcessInstanceClass from '../classes/ProcessInstanceClass';
export default function ProcessInstanceShow() { type OwnProps = {
variant: string;
};
export default function ProcessInstanceShow({ variant }: OwnProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const params = useParams(); const params = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -67,14 +72,21 @@ export default function ProcessInstanceShow() {
const modifiedProcessModelId = params.process_model_id; const modifiedProcessModelId = params.process_model_id;
const { targetUris } = useUriListForPermissions(); const { targetUris } = useUriListForPermissions();
const taskListPath =
variant === 'all'
? targetUris.processInstanceTaskListPath
: targetUris.processInstanceTaskListForMePath;
const permissionRequestData: PermissionsToCheck = { const permissionRequestData: PermissionsToCheck = {
[targetUris.messageInstanceListPath]: ['GET'], [targetUris.messageInstanceListPath]: ['GET'],
[targetUris.processInstanceTaskListPath]: ['GET'], [taskListPath]: ['GET'],
[targetUris.processInstanceTaskListDataPath]: ['GET', 'PUT'],
[targetUris.processInstanceActionPath]: ['DELETE'], [targetUris.processInstanceActionPath]: ['DELETE'],
[targetUris.processInstanceLogListPath]: ['GET'], [targetUris.processInstanceLogListPath]: ['GET'],
[`${targetUris.processInstanceActionPath}/suspend`]: ['PUT'], [targetUris.processModelShowPath]: ['PUT'],
[`${targetUris.processInstanceActionPath}/terminate`]: ['PUT'], [`${targetUris.processInstanceResumePath}`]: ['POST'],
[`${targetUris.processInstanceActionPath}/resume`]: ['PUT'], [`${targetUris.processInstanceSuspendPath}`]: ['POST'],
[`${targetUris.processInstanceTerminatePath}`]: ['POST'],
}; };
const { ability, permissionsLoaded } = usePermissionFetcher( const { ability, permissionsLoaded } = usePermissionFetcher(
permissionRequestData permissionRequestData
@ -96,17 +108,27 @@ export default function ProcessInstanceShow() {
if (processIdentifier) { if (processIdentifier) {
queryParams = `?process_identifier=${processIdentifier}`; queryParams = `?process_identifier=${processIdentifier}`;
} }
let apiPath = '/process-instances/for-me';
if (variant === 'all') {
apiPath = '/process-instances';
}
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}${queryParams}`, path: `${apiPath}/${modifiedProcessModelId}/${params.process_instance_id}${queryParams}`,
successCallback: setProcessInstance, successCallback: setProcessInstance,
}); });
let taskParams = '?all_tasks=true'; let taskParams = '?all_tasks=true';
if (typeof params.spiff_step !== 'undefined') { if (typeof params.spiff_step !== 'undefined') {
taskParams = `${taskParams}&spiff_step=${params.spiff_step}`; taskParams = `${taskParams}&spiff_step=${params.spiff_step}`;
} }
if (ability.can('GET', targetUris.processInstanceTaskListPath)) { let taskPath = '';
if (ability.can('GET', targetUris.processInstanceTaskListDataPath)) {
taskPath = `${targetUris.processInstanceTaskListDataPath}${taskParams}`;
} else if (ability.can('GET', taskListPath)) {
taskPath = `${taskListPath}${taskParams}`;
}
if (taskPath) {
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `${targetUris.processInstanceTaskListPath}${taskParams}`, path: taskPath,
successCallback: setTasks, successCallback: setTasks,
failureCallback: processTaskFailure, failureCallback: processTaskFailure,
}); });
@ -121,6 +143,8 @@ export default function ProcessInstanceShow() {
ability, ability,
targetUris, targetUris,
searchParams, searchParams,
taskListPath,
variant,
]); ]);
const deleteProcessInstance = () => { const deleteProcessInstance = () => {
@ -138,7 +162,7 @@ export default function ProcessInstanceShow() {
const terminateProcessInstance = () => { const terminateProcessInstance = () => {
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `${targetUris.processInstanceActionPath}/terminate`, path: `${targetUris.processInstanceTerminatePath}`,
successCallback: refreshPage, successCallback: refreshPage,
httpMethod: 'POST', httpMethod: 'POST',
}); });
@ -146,7 +170,7 @@ export default function ProcessInstanceShow() {
const suspendProcessInstance = () => { const suspendProcessInstance = () => {
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `${targetUris.processInstanceActionPath}/suspend`, path: `${targetUris.processInstanceSuspendPath}`,
successCallback: refreshPage, successCallback: refreshPage,
httpMethod: 'POST', httpMethod: 'POST',
}); });
@ -154,7 +178,7 @@ export default function ProcessInstanceShow() {
const resumeProcessInstance = () => { const resumeProcessInstance = () => {
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `${targetUris.processInstanceActionPath}/resume`, path: `${targetUris.processInstanceResumePath}`,
successCallback: refreshPage, successCallback: refreshPage,
httpMethod: 'POST', httpMethod: 'POST',
}); });
@ -175,29 +199,23 @@ export default function ProcessInstanceShow() {
return taskIds; return taskIds;
}; };
const currentSpiffStep = (processInstanceToUse: any) => { const currentSpiffStep = () => {
if (typeof params.spiff_step === 'undefined') { if (processInstance && typeof params.spiff_step === 'undefined') {
return processInstanceToUse.spiff_step; return processInstance.spiff_step || 0;
} }
return Number(params.spiff_step); return Number(params.spiff_step);
}; };
const showingFirstSpiffStep = (processInstanceToUse: any) => { const showingFirstSpiffStep = () => {
return currentSpiffStep(processInstanceToUse) === 1; return currentSpiffStep() === 1;
}; };
const showingLastSpiffStep = (processInstanceToUse: any) => { const showingLastSpiffStep = () => {
return ( return processInstance && currentSpiffStep() === processInstance.spiff_step;
currentSpiffStep(processInstanceToUse) === processInstanceToUse.spiff_step
);
}; };
const spiffStepLink = ( const spiffStepLink = (label: any, distance: number) => {
processInstanceToUse: any,
label: any,
distance: number
) => {
const processIdentifier = searchParams.get('process_identifier'); const processIdentifier = searchParams.get('process_identifier');
let queryParams = ''; let queryParams = '';
if (processIdentifier) { if (processIdentifier) {
@ -209,32 +227,35 @@ export default function ProcessInstanceShow() {
data-qa="process-instance-step-link" data-qa="process-instance-step-link"
to={`/admin/process-instances/${params.process_model_id}/${ to={`/admin/process-instances/${params.process_model_id}/${
params.process_instance_id params.process_instance_id
}/${currentSpiffStep(processInstanceToUse) + distance}${queryParams}`} }/${currentSpiffStep() + distance}${queryParams}`}
> >
{label} {label}
</Link> </Link>
); );
}; };
const previousStepLink = (processInstanceToUse: any) => { const previousStepLink = () => {
if (showingFirstSpiffStep(processInstanceToUse)) { if (showingFirstSpiffStep()) {
return null; return null;
} }
return spiffStepLink(processInstanceToUse, <CaretLeft />, -1); return spiffStepLink(<CaretLeft />, -1);
}; };
const nextStepLink = (processInstanceToUse: any) => { const nextStepLink = () => {
if (showingLastSpiffStep(processInstanceToUse)) { if (showingLastSpiffStep()) {
return null; return null;
} }
return spiffStepLink(processInstanceToUse, <CaretRight />, 1); return spiffStepLink(<CaretRight />, 1);
}; };
const getInfoTag = (processInstanceToUse: any) => { const getInfoTag = () => {
if (!processInstance) {
return null;
}
const currentEndDate = convertSecondsToFormattedDateTime( const currentEndDate = convertSecondsToFormattedDateTime(
processInstanceToUse.end_in_seconds processInstance.end_in_seconds || 0
); );
let currentEndDateTag; let currentEndDateTag;
if (currentEndDate) { if (currentEndDate) {
@ -245,7 +266,7 @@ export default function ProcessInstanceShow() {
</Column> </Column>
<Column sm={3} md={3} lg={3} className="grid-date"> <Column sm={3} md={3} lg={3} className="grid-date">
{convertSecondsToFormattedDateTime( {convertSecondsToFormattedDateTime(
processInstanceToUse.end_in_seconds processInstance.end_in_seconds || 0
) || 'N/A'} ) || 'N/A'}
</Column> </Column>
</Grid> </Grid>
@ -253,13 +274,13 @@ export default function ProcessInstanceShow() {
} }
let statusIcon = <InProgress />; let statusIcon = <InProgress />;
if (processInstanceToUse.status === 'suspended') { if (processInstance.status === 'suspended') {
statusIcon = <PauseOutline />; statusIcon = <PauseOutline />;
} else if (processInstanceToUse.status === 'complete') { } else if (processInstance.status === 'complete') {
statusIcon = <Checkmark />; statusIcon = <Checkmark />;
} else if (processInstanceToUse.status === 'terminated') { } else if (processInstance.status === 'terminated') {
statusIcon = <StopOutline />; statusIcon = <StopOutline />;
} else if (processInstanceToUse.status === 'error') { } else if (processInstance.status === 'error') {
statusIcon = <Warning />; statusIcon = <Warning />;
} }
@ -271,7 +292,7 @@ export default function ProcessInstanceShow() {
</Column> </Column>
<Column sm={3} md={3} lg={3} className="grid-date"> <Column sm={3} md={3} lg={3} className="grid-date">
{convertSecondsToFormattedDateTime( {convertSecondsToFormattedDateTime(
processInstanceToUse.start_in_seconds processInstance.start_in_seconds || 0
)} )}
</Column> </Column>
</Grid> </Grid>
@ -282,7 +303,7 @@ export default function ProcessInstanceShow() {
</Column> </Column>
<Column sm={3} md={3} lg={3}> <Column sm={3} md={3} lg={3}>
<Tag type="gray" size="sm" className="span-tag"> <Tag type="gray" size="sm" className="span-tag">
{processInstanceToUse.status} {statusIcon} {processInstance.status} {statusIcon}
</Tag> </Tag>
</Column> </Column>
</Grid> </Grid>
@ -325,11 +346,10 @@ export default function ProcessInstanceShow() {
); );
}; };
const terminateButton = (processInstanceToUse: any) => { const terminateButton = () => {
if ( if (
['complete', 'terminated', 'error'].indexOf( processInstance &&
processInstanceToUse.status !ProcessInstanceClass.terminalStatuses().includes(processInstance.status)
) === -1
) { ) {
return ( return (
<ButtonWithConfirmation <ButtonWithConfirmation
@ -337,7 +357,7 @@ export default function ProcessInstanceShow() {
renderIcon={StopOutline} renderIcon={StopOutline}
iconDescription="Terminate" iconDescription="Terminate"
hasIconOnly hasIconOnly
description={`Terminate Process Instance: ${processInstanceToUse.id}`} description={`Terminate Process Instance: ${processInstance.id}`}
onConfirmation={terminateProcessInstance} onConfirmation={terminateProcessInstance}
confirmButtonLabel="Terminate" confirmButtonLabel="Terminate"
/> />
@ -346,11 +366,12 @@ export default function ProcessInstanceShow() {
return <div />; return <div />;
}; };
const suspendButton = (processInstanceToUse: any) => { const suspendButton = () => {
if ( if (
['complete', 'terminated', 'error', 'suspended'].indexOf( processInstance &&
processInstanceToUse.status !ProcessInstanceClass.terminalStatuses()
) === -1 .concat(['suspended'])
.includes(processInstance.status)
) { ) {
return ( return (
<Button <Button
@ -366,8 +387,8 @@ export default function ProcessInstanceShow() {
return <div />; return <div />;
}; };
const resumeButton = (processInstanceToUse: any) => { const resumeButton = () => {
if (processInstanceToUse.status === 'suspended') { if (processInstance && processInstance.status === 'suspended') {
return ( return (
<Button <Button
onClick={resumeProcessInstance} onClick={resumeProcessInstance}
@ -392,13 +413,13 @@ export default function ProcessInstanceShow() {
const handleClickedDiagramTask = ( const handleClickedDiagramTask = (
shapeElement: any, shapeElement: any,
bpmnRootElement: any bpmnProcessIdentifiers: any
) => { ) => {
if (tasks) { if (tasks) {
const matchingTask: any = tasks.find( const matchingTask: any = tasks.find(
(task: any) => (task: any) =>
task.name === shapeElement.id && task.name === shapeElement.id &&
task.process_identifier === bpmnRootElement.id bpmnProcessIdentifiers.includes(task.process_identifier)
); );
if (matchingTask) { if (matchingTask) {
setTaskToDisplay(matchingTask); setTaskToDisplay(matchingTask);
@ -442,7 +463,11 @@ export default function ProcessInstanceShow() {
const canEditTaskData = (task: any) => { const canEditTaskData = (task: any) => {
return ( return (
task.state === 'READY' && showingLastSpiffStep(processInstance as any) processInstance &&
ability.can('PUT', targetUris.processInstanceTaskListDataPath) &&
task.state === 'READY' &&
processInstance.status === 'suspended' &&
showingLastSpiffStep()
); );
}; };
@ -465,7 +490,7 @@ export default function ProcessInstanceShow() {
}; };
const saveTaskDataFailure = (result: any) => { const saveTaskDataFailure = (result: any) => {
setErrorMessage({ message: result.toString() }); setErrorMessage({ message: result.message });
}; };
const saveTaskData = () => { const saveTaskData = () => {
@ -491,7 +516,10 @@ export default function ProcessInstanceShow() {
const taskDataButtons = (task: any) => { const taskDataButtons = (task: any) => {
const buttons = []; const buttons = [];
if (task.type === 'Script Task') { if (
task.type === 'Script Task' &&
ability.can('PUT', targetUris.processModelShowPath)
) {
buttons.push( buttons.push(
<Button <Button
data-qa="create-script-unit-test-button" data-qa="create-script-unit-test-button"
@ -578,37 +606,41 @@ export default function ProcessInstanceShow() {
return null; return null;
}; };
const stepsElement = (processInstanceToUse: any) => { const stepsElement = () => {
if (!processInstance) {
return null;
}
return ( return (
<Grid condensed fullWidth> <Grid condensed fullWidth>
<Column sm={3} md={3} lg={3}> <Column sm={3} md={3} lg={3}>
<Stack orientation="horizontal" gap={3} className="smaller-text"> <Stack orientation="horizontal" gap={3} className="smaller-text">
{previousStepLink(processInstanceToUse)} {previousStepLink()}
Step {currentSpiffStep(processInstanceToUse)} of{' '} Step {currentSpiffStep()} of {processInstance.spiff_step}
{processInstanceToUse.spiff_step} {nextStepLink()}
{nextStepLink(processInstanceToUse)}
</Stack> </Stack>
</Column> </Column>
</Grid> </Grid>
); );
}; };
const buttonIcons = (processInstanceToUse: any) => { const buttonIcons = () => {
if (!processInstance) {
return null;
}
const elements = []; const elements = [];
if ( if (ability.can('POST', `${targetUris.processInstanceTerminatePath}`)) {
ability.can('POST', `${targetUris.processInstanceActionPath}/terminate`) elements.push(terminateButton());
) { }
elements.push(terminateButton(processInstanceToUse)); if (ability.can('POST', `${targetUris.processInstanceSuspendPath}`)) {
elements.push(suspendButton());
}
if (ability.can('POST', `${targetUris.processInstanceResumePath}`)) {
elements.push(resumeButton());
} }
if ( if (
ability.can('POST', `${targetUris.processInstanceActionPath}/suspend`) ability.can('DELETE', targetUris.processInstanceActionPath) &&
ProcessInstanceClass.terminalStatuses().includes(processInstance.status)
) { ) {
elements.push(suspendButton(processInstanceToUse));
}
if (ability.can('POST', `${targetUris.processInstanceActionPath}/resume`)) {
elements.push(resumeButton(processInstanceToUse));
}
if (ability.can('DELETE', targetUris.processInstanceActionPath)) {
elements.push( elements.push(
<ButtonWithConfirmation <ButtonWithConfirmation
data-qa="process-instance-delete" data-qa="process-instance-delete"
@ -616,7 +648,7 @@ export default function ProcessInstanceShow() {
renderIcon={TrashCan} renderIcon={TrashCan}
iconDescription="Delete" iconDescription="Delete"
hasIconOnly hasIconOnly
description={`Delete Process Instance: ${processInstanceToUse.id}`} description={`Delete Process Instance: ${processInstance.id}`}
onConfirmation={deleteProcessInstance} onConfirmation={deleteProcessInstance}
confirmButtonLabel="Delete" confirmButtonLabel="Delete"
/> />
@ -626,7 +658,6 @@ export default function ProcessInstanceShow() {
}; };
if (processInstance && (tasks || tasksCallHadError)) { if (processInstance && (tasks || tasksCallHadError)) {
const processInstanceToUse = processInstance as any;
const taskIds = getTaskIds(); const taskIds = getTaskIds();
const processModelId = unModifyProcessIdentifierForPathParam( const processModelId = unModifyProcessIdentifierForPathParam(
params.process_model_id ? params.process_model_id : '' params.process_model_id ? params.process_model_id : ''
@ -642,26 +673,26 @@ export default function ProcessInstanceShow() {
entityType: 'process-model-id', entityType: 'process-model-id',
linkLastItem: true, linkLastItem: true,
}, },
[`Process Instance Id: ${processInstanceToUse.id}`], [`Process Instance Id: ${processInstance.id}`],
]} ]}
/> />
<Stack orientation="horizontal" gap={1}> <Stack orientation="horizontal" gap={1}>
<h1 className="with-icons"> <h1 className="with-icons">
Process Instance Id: {processInstanceToUse.id} Process Instance Id: {processInstance.id}
</h1> </h1>
{buttonIcons(processInstanceToUse)} {buttonIcons()}
</Stack> </Stack>
<br /> <br />
<br /> <br />
{getInfoTag(processInstanceToUse)} {getInfoTag()}
<br /> <br />
{taskDataDisplayArea()} {taskDataDisplayArea()}
{stepsElement(processInstanceToUse)} {stepsElement()}
<br /> <br />
<ReactDiagramEditor <ReactDiagramEditor
processModelId={processModelId || ''} processModelId={processModelId || ''}
diagramXML={processInstanceToUse.bpmn_xml_file_contents || ''} diagramXML={processInstance.bpmn_xml_file_contents || ''}
fileName={processInstanceToUse.bpmn_xml_file_contents || ''} fileName={processInstance.bpmn_xml_file_contents || ''}
readyOrWaitingProcessInstanceTasks={taskIds.readyOrWaiting} readyOrWaitingProcessInstanceTasks={taskIds.readyOrWaiting}
completedProcessInstanceTasks={taskIds.completed} completedProcessInstanceTasks={taskIds.completed}
diagramType="readonly" diagramType="readonly"

View File

@ -25,6 +25,7 @@ import {
ProcessReference, ProcessReference,
} from '../interfaces'; } from '../interfaces';
import ProcessSearch from '../components/ProcessSearch'; import ProcessSearch from '../components/ProcessSearch';
import { Notification } from '../components/Notification';
export default function ProcessModelEditDiagram() { export default function ProcessModelEditDiagram() {
const [showFileNameEditor, setShowFileNameEditor] = useState(false); const [showFileNameEditor, setShowFileNameEditor] = useState(false);
@ -157,6 +158,8 @@ export default function ProcessModelEditDiagram() {
} }
}; };
const [displaySaveFileMessage, setDisplaySaveFileMessage] =
useState<boolean>(false);
const saveDiagram = (bpmnXML: any, fileName = params.file_name) => { const saveDiagram = (bpmnXML: any, fileName = params.file_name) => {
setErrorMessage(null); setErrorMessage(null);
setBpmnXmlForDiagramRendering(bpmnXML); setBpmnXmlForDiagramRendering(bpmnXML);
@ -192,6 +195,7 @@ export default function ProcessModelEditDiagram() {
// after saving the file, make sure we null out newFileName // after saving the file, make sure we null out newFileName
// so it does not get used over the params // so it does not get used over the params
setNewFileName(''); setNewFileName('');
setDisplaySaveFileMessage(true);
}; };
const onDeleteFile = (fileName = params.file_name) => { const onDeleteFile = (fileName = params.file_name) => {
@ -819,6 +823,7 @@ export default function ProcessModelEditDiagram() {
processModelId={params.process_model_id || ''} processModelId={params.process_model_id || ''}
saveDiagram={saveDiagram} saveDiagram={saveDiagram}
onDeleteFile={onDeleteFile} onDeleteFile={onDeleteFile}
isPrimaryFile={params.file_name === processModel?.primary_file_name}
onSetPrimaryFile={onSetPrimaryFileCallback} onSetPrimaryFile={onSetPrimaryFileCallback}
diagramXML={bpmnXmlForDiagramRendering} diagramXML={bpmnXmlForDiagramRendering}
fileName={params.file_name} fileName={params.file_name}
@ -836,6 +841,20 @@ export default function ProcessModelEditDiagram() {
); );
}; };
const saveFileMessage = () => {
if (displaySaveFileMessage) {
return (
<Notification
title="File Saved: "
onClose={() => setDisplaySaveFileMessage(false)}
>
Changes to the file were saved.
</Notification>
);
}
return null;
};
// if a file name is not given then this is a new model and the ReactDiagramEditor component will handle it // if a file name is not given then this is a new model and the ReactDiagramEditor component will handle it
if ((bpmnXmlForDiagramRendering || !params.file_name) && processModel) { if ((bpmnXmlForDiagramRendering || !params.file_name) && processModel) {
const processModelFileName = processModelFile ? processModelFile.name : ''; const processModelFileName = processModelFile ? processModelFile.name : '';
@ -856,6 +875,7 @@ export default function ProcessModelEditDiagram() {
Process Model File{processModelFile ? ': ' : ''} Process Model File{processModelFile ? ': ' : ''}
{processModelFileName} {processModelFileName}
</h1> </h1>
{saveFileMessage()}
{appropriateEditor()} {appropriateEditor()}
{newFileNameBox()} {newFileNameBox()}
{scriptEditor()} {scriptEditor()}

View File

@ -264,6 +264,7 @@ export default function ProcessModelShow() {
</Can> </Can>
); );
if (!isPrimaryBpmnFile) {
elements.push( elements.push(
<Can <Can
I="DELETE" I="DELETE"
@ -283,6 +284,7 @@ export default function ProcessModelShow() {
/> />
</Can> </Can>
); );
}
if (processModelFile.name.match(/\.bpmn$/) && !isPrimaryBpmnFile) { if (processModelFile.name.match(/\.bpmn$/) && !isPrimaryBpmnFile) {
elements.push( elements.push(
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}> <Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
@ -360,13 +362,12 @@ export default function ProcessModelShow() {
); );
}; };
const handleFileUploadCancel = () => { const [fileUploadEvent, setFileUploadEvent] = useState(null);
setShowFileUploadModal(false); const [duplicateFilename, setDuplicateFilename] = useState<String>('');
setFilesToUpload(null); const [showOverwriteConfirmationPrompt, setShowOverwriteConfirmationPrompt] =
}; useState(false);
const handleFileUpload = (event: any) => { const doFileUpload = (event: any) => {
if (processModel) {
event.preventDefault(); event.preventDefault();
const url = `/process-models/${modifiedProcessModelId}/files`; const url = `/process-models/${modifiedProcessModelId}/files`;
const formData = new FormData(); const formData = new FormData();
@ -378,10 +379,67 @@ export default function ProcessModelShow() {
httpMethod: 'POST', httpMethod: 'POST',
postBody: formData, postBody: formData,
}); });
} setFilesToUpload(null);
};
const handleFileUploadCancel = () => {
setShowFileUploadModal(false); setShowFileUploadModal(false);
setFilesToUpload(null); setFilesToUpload(null);
}; };
const handleOverwriteFileConfirm = () => {
setShowOverwriteConfirmationPrompt(false);
doFileUpload(fileUploadEvent);
};
const handleOverwriteFileCancel = () => {
setShowOverwriteConfirmationPrompt(false);
setFilesToUpload(null);
};
const confirmOverwriteFileDialog = () => {
return (
<Modal
danger
open={showOverwriteConfirmationPrompt}
data-qa="file-overwrite-modal-confirmation-dialog"
modalHeading={`Overwrite the file: ${duplicateFilename}`}
modalLabel="Overwrite file?"
primaryButtonText="Yes"
secondaryButtonText="Cancel"
onSecondarySubmit={handleOverwriteFileCancel}
onRequestSubmit={handleOverwriteFileConfirm}
onRequestClose={handleOverwriteFileCancel}
/>
);
};
const displayOverwriteConfirmation = (filename: String) => {
setDuplicateFilename(filename);
setShowOverwriteConfirmationPrompt(true);
};
const checkDuplicateFile = (event: any) => {
if (processModel) {
let foundExistingFile = false;
if (processModel.files.length > 0) {
processModel.files.forEach((file) => {
if (file.name === filesToUpload[0].name) {
foundExistingFile = true;
}
});
}
if (foundExistingFile) {
displayOverwriteConfirmation(filesToUpload[0].name);
setFileUploadEvent(event);
} else {
doFileUpload(event);
}
}
return null;
};
const handleFileUpload = (event: any) => {
checkDuplicateFile(event);
setShowFileUploadModal(false);
};
const fileUploadModal = () => { const fileUploadModal = () => {
return ( return (
@ -416,9 +474,6 @@ export default function ProcessModelShow() {
}; };
const processModelFilesSection = () => { const processModelFilesSection = () => {
if (!processModel) {
return null;
}
return ( return (
<Grid <Grid
condensed condensed
@ -500,7 +555,7 @@ export default function ProcessModelShow() {
return ( return (
<Grid fullWidth condensed> <Grid fullWidth condensed>
<Column sm={{ span: 3 }} md={{ span: 4 }} lg={{ span: 3 }}> <Column sm={{ span: 3 }} md={{ span: 4 }} lg={{ span: 3 }}>
<h2>Process Instances</h2> <h2>My Process Instances</h2>
</Column> </Column>
<Column <Column
sm={{ span: 1, offset: 3 }} sm={{ span: 1, offset: 3 }}
@ -548,6 +603,7 @@ export default function ProcessModelShow() {
return ( return (
<> <>
{fileUploadModal()} {fileUploadModal()}
{confirmOverwriteFileDialog()}
<ProcessBreadcrumb <ProcessBreadcrumb
hotCrumbs={[ hotCrumbs={[
['Process Groups', '/admin'], ['Process Groups', '/admin'],
@ -619,6 +675,7 @@ export default function ProcessModelShow() {
{processInstanceListTableButton()} {processInstanceListTableButton()}
<ProcessInstanceListTable <ProcessInstanceListTable
filtersEnabled={false} filtersEnabled={false}
variant="for-me"
processModelFullIdentifier={processModel.id} processModelFullIdentifier={processModel.id}
perPageOptions={[2, 5, 25]} perPageOptions={[2, 5, 25]}
showReports={false} showReports={false}

View File

@ -36,7 +36,20 @@ export default function ReactFormEditor() {
return searchParams.get('file_ext') ?? 'json'; return searchParams.get('file_ext') ?? 'json';
})(); })();
const editorDefaultLanguage = fileExtension === 'md' ? 'markdown' : 'json'; const hasDiagram = fileExtension === 'bpmn' || fileExtension === 'dmn';
const editorDefaultLanguage = (() => {
if (fileExtension === 'json') {
return 'json';
}
if (hasDiagram) {
return 'xml';
}
if (fileExtension === 'md') {
return 'markdown';
}
return 'text';
})();
const modifiedProcessModelId = modifyProcessIdentifierForPathParam( const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
`${params.process_model_id}` `${params.process_model_id}`
@ -193,6 +206,19 @@ export default function ReactFormEditor() {
buttonLabel="Delete" buttonLabel="Delete"
/> />
) : null} ) : null}
{hasDiagram ? (
<Button
onClick={() =>
navigate(
`/admin/process-models/${modifiedProcessModelId}/files/${params.file_name}`
)
}
variant="danger"
data-qa="view-diagram-button"
>
View Diagram
</Button>
) : null}
<Editor <Editor
height={600} height={600}
width="auto" width="auto"

View File

@ -40,7 +40,7 @@ export default function TaskShow() {
const { targetUris } = useUriListForPermissions(); const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = { const permissionRequestData: PermissionsToCheck = {
[targetUris.processInstanceTaskListPath]: ['GET'], [targetUris.processInstanceTaskListDataPath]: ['GET'],
}; };
const { ability, permissionsLoaded } = usePermissionFetcher( const { ability, permissionsLoaded } = usePermissionFetcher(
permissionRequestData permissionRequestData
@ -50,7 +50,7 @@ export default function TaskShow() {
if (permissionsLoaded) { if (permissionsLoaded) {
const processResult = (result: any) => { const processResult = (result: any) => {
setTask(result); setTask(result);
if (ability.can('GET', targetUris.processInstanceTaskListPath)) { if (ability.can('GET', targetUris.processInstanceTaskListDataPath)) {
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/task-data/${modifyProcessIdentifierForPathParam( path: `/task-data/${modifyProcessIdentifierForPathParam(
result.process_model_identifier result.process_model_identifier
@ -171,7 +171,6 @@ export default function TaskShow() {
} else if (taskToUse.form_ui_schema) { } else if (taskToUse.form_ui_schema) {
formUiSchema = JSON.parse(taskToUse.form_ui_schema); formUiSchema = JSON.parse(taskToUse.form_ui_schema);
} }
if (taskToUse.state !== 'READY') { if (taskToUse.state !== 'READY') {
formUiSchema = Object.assign(formUiSchema || {}, { formUiSchema = Object.assign(formUiSchema || {}, {
'ui:readonly': true, 'ui:readonly': true,
@ -184,7 +183,7 @@ export default function TaskShow() {
reactFragmentToHideSubmitButton = <div />; reactFragmentToHideSubmitButton = <div />;
} }
if (taskToUse.type === 'Manual Task') { if (taskToUse.type === 'Manual Task' && taskToUse.state === 'READY') {
reactFragmentToHideSubmitButton = ( reactFragmentToHideSubmitButton = (
<div> <div>
<Button type="submit">Continue</Button> <Button type="submit">Continue</Button>

View File

@ -26,7 +26,7 @@ type backendCallProps = {
postBody?: any; postBody?: any;
}; };
class UnauthenticatedError extends Error { export class UnauthenticatedError extends Error {
constructor(message: string) { constructor(message: string) {
super(message); super(message);
this.name = 'UnauthenticatedError'; this.name = 'UnauthenticatedError';

View File

@ -27,8 +27,8 @@ const doLogout = () => {
const idToken = getIdToken(); const idToken = getIdToken();
localStorage.removeItem('jwtAccessToken'); localStorage.removeItem('jwtAccessToken');
localStorage.removeItem('jwtIdToken'); localStorage.removeItem('jwtIdToken');
const redirctUrl = `${window.location.origin}/`; const redirectUrl = `${window.location.origin}`;
const url = `${BACKEND_BASE_URL}/logout?redirect_url=${redirctUrl}&id_token=${idToken}`; const url = `${BACKEND_BASE_URL}/logout?redirect_url=${redirectUrl}&id_token=${idToken}`;
window.location.href = url; window.location.href = url;
}; };