Merge pull request #193 from sartography/feature/remove_spif_step_details

Feature/remove spif step details
This commit is contained in:
jasquat 2023-03-24 11:16:43 -04:00 committed by GitHub
commit 85967315b5
35 changed files with 1072 additions and 911 deletions

View File

@ -47,7 +47,8 @@ def app() -> Flask:
def with_db_and_bpmn_file_cleanup() -> None: def with_db_and_bpmn_file_cleanup() -> None:
"""Do it cleanly!""" """Do it cleanly!"""
meta = db.metadata meta = db.metadata
db.session.execute(db.update(BpmnProcessModel, values={"parent_process_id": None})) db.session.execute(db.update(BpmnProcessModel, values={"top_level_process_id": None}))
db.session.execute(db.update(BpmnProcessModel, values={"direct_parent_process_id": None}))
for table in reversed(meta.sorted_tables): for table in reversed(meta.sorted_tables):
db.session.execute(table.delete()) db.session.execute(table.delete())

View File

@ -1,8 +1,8 @@
"""empty message """empty message
Revision ID: b652c232839f Revision ID: 0b5dd14bfbac
Revises: Revises:
Create Date: 2023-03-17 16:50:32.774216 Create Date: 2023-03-23 16:25:33.288500
""" """
from alembic import op from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
from sqlalchemy.dialects import mysql from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'b652c232839f' revision = '0b5dd14bfbac'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -115,19 +115,22 @@ def upgrade():
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('guid', sa.String(length=36), nullable=True), sa.Column('guid', sa.String(length=36), nullable=True),
sa.Column('bpmn_process_definition_id', sa.Integer(), nullable=False), sa.Column('bpmn_process_definition_id', sa.Integer(), nullable=False),
sa.Column('parent_process_id', sa.Integer(), nullable=True), sa.Column('top_level_process_id', sa.Integer(), nullable=True),
sa.Column('direct_parent_process_id', sa.Integer(), nullable=True),
sa.Column('properties_json', sa.JSON(), nullable=False), sa.Column('properties_json', sa.JSON(), nullable=False),
sa.Column('json_data_hash', sa.String(length=255), nullable=False), sa.Column('json_data_hash', sa.String(length=255), nullable=False),
sa.Column('start_in_seconds', sa.DECIMAL(precision=17, scale=6), nullable=True), sa.Column('start_in_seconds', sa.DECIMAL(precision=17, scale=6), nullable=True),
sa.Column('end_in_seconds', sa.DECIMAL(precision=17, scale=6), nullable=True), sa.Column('end_in_seconds', sa.DECIMAL(precision=17, scale=6), nullable=True),
sa.ForeignKeyConstraint(['bpmn_process_definition_id'], ['bpmn_process_definition.id'], ), sa.ForeignKeyConstraint(['bpmn_process_definition_id'], ['bpmn_process_definition.id'], ),
sa.ForeignKeyConstraint(['parent_process_id'], ['bpmn_process.id'], ), sa.ForeignKeyConstraint(['direct_parent_process_id'], ['bpmn_process.id'], ),
sa.ForeignKeyConstraint(['top_level_process_id'], ['bpmn_process.id'], ),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('guid') sa.UniqueConstraint('guid')
) )
op.create_index(op.f('ix_bpmn_process_bpmn_process_definition_id'), 'bpmn_process', ['bpmn_process_definition_id'], unique=False) op.create_index(op.f('ix_bpmn_process_bpmn_process_definition_id'), 'bpmn_process', ['bpmn_process_definition_id'], unique=False)
op.create_index(op.f('ix_bpmn_process_direct_parent_process_id'), 'bpmn_process', ['direct_parent_process_id'], unique=False)
op.create_index(op.f('ix_bpmn_process_json_data_hash'), 'bpmn_process', ['json_data_hash'], unique=False) op.create_index(op.f('ix_bpmn_process_json_data_hash'), 'bpmn_process', ['json_data_hash'], unique=False)
op.create_index(op.f('ix_bpmn_process_parent_process_id'), 'bpmn_process', ['parent_process_id'], unique=False) op.create_index(op.f('ix_bpmn_process_top_level_process_id'), 'bpmn_process', ['top_level_process_id'], unique=False)
op.create_table('bpmn_process_definition_relationship', op.create_table('bpmn_process_definition_relationship',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('bpmn_process_definition_parent_id', sa.Integer(), nullable=False), sa.Column('bpmn_process_definition_parent_id', sa.Integer(), nullable=False),
@ -248,7 +251,6 @@ def upgrade():
sa.Column('status', sa.String(length=50), nullable=True), sa.Column('status', sa.String(length=50), nullable=True),
sa.Column('bpmn_version_control_type', sa.String(length=50), nullable=True), sa.Column('bpmn_version_control_type', sa.String(length=50), nullable=True),
sa.Column('bpmn_version_control_identifier', sa.String(length=255), nullable=True), sa.Column('bpmn_version_control_identifier', sa.String(length=255), nullable=True),
sa.Column('spiff_step', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['bpmn_process_definition_id'], ['bpmn_process_definition.id'], ), sa.ForeignKeyConstraint(['bpmn_process_definition_id'], ['bpmn_process_definition.id'], ),
sa.ForeignKeyConstraint(['bpmn_process_id'], ['bpmn_process.id'], ), sa.ForeignKeyConstraint(['bpmn_process_id'], ['bpmn_process.id'], ),
sa.ForeignKeyConstraint(['process_initiator_id'], ['user.id'], ), sa.ForeignKeyConstraint(['process_initiator_id'], ['user.id'], ),
@ -344,22 +346,6 @@ def upgrade():
op.create_index(op.f('ix_process_instance_queue_locked_at_in_seconds'), 'process_instance_queue', ['locked_at_in_seconds'], unique=False) op.create_index(op.f('ix_process_instance_queue_locked_at_in_seconds'), 'process_instance_queue', ['locked_at_in_seconds'], unique=False)
op.create_index(op.f('ix_process_instance_queue_locked_by'), 'process_instance_queue', ['locked_by'], unique=False) op.create_index(op.f('ix_process_instance_queue_locked_by'), 'process_instance_queue', ['locked_by'], unique=False)
op.create_index(op.f('ix_process_instance_queue_status'), 'process_instance_queue', ['status'], unique=False) op.create_index(op.f('ix_process_instance_queue_status'), 'process_instance_queue', ['status'], unique=False)
op.create_table('spiff_step_details',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('process_instance_id', sa.Integer(), nullable=False),
sa.Column('spiff_step', sa.Integer(), nullable=False),
sa.Column('task_json', sa.JSON(), nullable=False),
sa.Column('task_id', sa.String(length=50), nullable=False),
sa.Column('task_state', sa.String(length=50), nullable=False),
sa.Column('bpmn_task_identifier', sa.String(length=255), nullable=False),
sa.Column('delta_json', sa.JSON(), nullable=True),
sa.Column('start_in_seconds', sa.DECIMAL(precision=17, scale=6), nullable=False),
sa.Column('end_in_seconds', sa.DECIMAL(precision=17, scale=6), nullable=True),
sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('process_instance_id', 'spiff_step', name='process_instance_id_spiff_step')
)
op.create_index(op.f('ix_spiff_step_details_process_instance_id'), 'spiff_step_details', ['process_instance_id'], unique=False)
op.create_table('task', op.create_table('task',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('guid', sa.String(length=36), nullable=False), sa.Column('guid', sa.String(length=36), nullable=False),
@ -465,8 +451,6 @@ def downgrade():
op.drop_index(op.f('ix_task_json_data_hash'), table_name='task') op.drop_index(op.f('ix_task_json_data_hash'), table_name='task')
op.drop_index(op.f('ix_task_bpmn_process_id'), table_name='task') op.drop_index(op.f('ix_task_bpmn_process_id'), table_name='task')
op.drop_table('task') op.drop_table('task')
op.drop_index(op.f('ix_spiff_step_details_process_instance_id'), table_name='spiff_step_details')
op.drop_table('spiff_step_details')
op.drop_index(op.f('ix_process_instance_queue_status'), table_name='process_instance_queue') op.drop_index(op.f('ix_process_instance_queue_status'), table_name='process_instance_queue')
op.drop_index(op.f('ix_process_instance_queue_locked_by'), table_name='process_instance_queue') op.drop_index(op.f('ix_process_instance_queue_locked_by'), table_name='process_instance_queue')
op.drop_index(op.f('ix_process_instance_queue_locked_at_in_seconds'), table_name='process_instance_queue') op.drop_index(op.f('ix_process_instance_queue_locked_at_in_seconds'), table_name='process_instance_queue')
@ -519,8 +503,9 @@ def downgrade():
op.drop_index(op.f('ix_bpmn_process_definition_relationship_bpmn_process_definition_child_id'), table_name='bpmn_process_definition_relationship') op.drop_index(op.f('ix_bpmn_process_definition_relationship_bpmn_process_definition_child_id'), table_name='bpmn_process_definition_relationship')
op.drop_index(op.f('ix_bpmn_process_definition_relationship_bpmn_process_definition_parent_id'), table_name='bpmn_process_definition_relationship') op.drop_index(op.f('ix_bpmn_process_definition_relationship_bpmn_process_definition_parent_id'), table_name='bpmn_process_definition_relationship')
op.drop_table('bpmn_process_definition_relationship') op.drop_table('bpmn_process_definition_relationship')
op.drop_index(op.f('ix_bpmn_process_parent_process_id'), table_name='bpmn_process') op.drop_index(op.f('ix_bpmn_process_top_level_process_id'), table_name='bpmn_process')
op.drop_index(op.f('ix_bpmn_process_json_data_hash'), table_name='bpmn_process') op.drop_index(op.f('ix_bpmn_process_json_data_hash'), table_name='bpmn_process')
op.drop_index(op.f('ix_bpmn_process_direct_parent_process_id'), table_name='bpmn_process')
op.drop_index(op.f('ix_bpmn_process_bpmn_process_definition_id'), table_name='bpmn_process') op.drop_index(op.f('ix_bpmn_process_bpmn_process_definition_id'), table_name='bpmn_process')
op.drop_table('bpmn_process') op.drop_table('bpmn_process')
op.drop_index(op.f('ix_user_service_id'), table_name='user') op.drop_index(op.f('ix_user_service_id'), table_name='user')

View File

@ -901,24 +901,24 @@ paths:
description: The identifier of the process to use for the diagram. Useful for displaying the diagram for a call activity. description: The identifier of the process to use for the diagram. Useful for displaying the diagram for a call activity.
schema: schema:
type: string 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
- name: most_recent_tasks_only - name: most_recent_tasks_only
in: query in: query
required: false required: false
description: If true, this wil return only the most recent tasks. description: If true, this wil return only the most recent tasks.
schema: schema:
type: boolean type: boolean
- name: bpmn_process_guid
in: query
required: false
description: The guid of the bpmn process to get the tasks for.
schema:
type: string
- name: to_task_guid
in: query
required: false
description: Get the tasks only up to the given guid.
schema:
type: string
get: get:
tags: tags:
- Process Instances - Process Instances
@ -954,24 +954,24 @@ paths:
description: The identifier of the process to use for the diagram. Useful for displaying the diagram for a call activity. description: The identifier of the process to use for the diagram. Useful for displaying the diagram for a call activity.
schema: schema:
type: string 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
- name: most_recent_tasks_only - name: most_recent_tasks_only
in: query in: query
required: false required: false
description: If true, this wil return only the most recent tasks. description: If true, this wil return only the most recent tasks.
schema: schema:
type: boolean type: boolean
- name: bpmn_process_guid
in: query
required: false
description: The guid of the bpmn process to get the tasks for.
schema:
type: string
- name: to_task_guid
in: query
required: false
description: Get the tasks only up to the given guid.
schema:
type: string
get: get:
tags: tags:
- Process Instances - Process Instances
@ -1176,7 +1176,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/OkTrue" $ref: "#/components/schemas/OkTrue"
/process-instance-reset/{modified_process_model_identifier}/{process_instance_id}/{spiff_step}: /process-instance-reset/{modified_process_model_identifier}/{process_instance_id}/{to_task_guid}:
parameters: parameters:
- name: modified_process_model_identifier - name: modified_process_model_identifier
in: path in: path
@ -1190,12 +1190,12 @@ paths:
description: The unique id of an existing process instance. description: The unique id of an existing process instance.
schema: schema:
type: integer type: integer
- name: spiff_step - name: to_task_guid
in: query in: path
required: false required: true
description: Reset the process to this state description: Get the tasks only up to the given guid.
schema: schema:
type: integer type: string
post: post:
operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_reset operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_reset
summary: Reset a process instance to an earlier step summary: Reset a process instance to an earlier step
@ -1573,7 +1573,7 @@ paths:
items: items:
$ref: "#/components/schemas/Task" $ref: "#/components/schemas/Task"
/task-data/{modified_process_model_identifier}/{process_instance_id}/{spiff_step}: /task-data/{modified_process_model_identifier}/{process_instance_id}/{task_guid}:
parameters: parameters:
- name: modified_process_model_identifier - name: modified_process_model_identifier
in: path in: path
@ -1587,15 +1587,15 @@ paths:
description: The unique id of an existing process instance. description: The unique id of an existing process instance.
schema: schema:
type: integer type: integer
- name: spiff_step - name: task_guid
in: path in: path
required: true required: true
description: If set will return the tasks as they were during a specific step of execution. description: The unique id of the task.
schema: schema:
type: integer type: string
get: get:
operationId: spiffworkflow_backend.routes.tasks_controller.task_data_show operationId: spiffworkflow_backend.routes.tasks_controller.task_data_show
summary: Get task data for a single task in a spiff step. summary: Get task data for a single task.
tags: tags:
- Process Instances - Process Instances
responses: responses:
@ -1605,35 +1605,8 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/Task" $ref: "#/components/schemas/Task"
/task-data/{modified_process_model_identifier}/{process_instance_id}/{task_id}:
parameters:
- name: modified_process_model_identifier
in: path
required: true
description: The modified 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: task_id
in: path
required: true
description: The unique id of the task.
schema:
type: string
- 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
put: put:
operationId: spiffworkflow_backend.routes.process_api_blueprint.task_data_update operationId: spiffworkflow_backend.routes.tasks_controller.task_data_update
summary: Update the task data for requested instance and task summary: Update the task data for requested instance and task
tags: tags:
- Process Instances - Process Instances
@ -1738,7 +1711,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/Workflow" $ref: "#/components/schemas/Workflow"
/task-complete/{modified_process_model_identifier}/{process_instance_id}/{task_id}: /task-complete/{modified_process_model_identifier}/{process_instance_id}/{task_guid}:
parameters: parameters:
- name: modified_process_model_identifier - name: modified_process_model_identifier
in: path in: path
@ -1752,14 +1725,14 @@ paths:
description: The unique id of the process instance description: The unique id of the process instance
schema: schema:
type: string type: string
- name: task_id - name: task_guid
in: path in: path
required: true required: true
description: The unique id of the task. description: The unique id of the task.
schema: schema:
type: string type: string
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.manual_complete_task operationId: spiffworkflow_backend.routes.tasks_controller.manual_complete_task
summary: Mark a task complete without executing it summary: Mark a task complete without executing it
tags: tags:
- Process Instances - Process Instances
@ -1838,9 +1811,9 @@ paths:
schema: schema:
$ref: "#/components/schemas/ServiceTask" $ref: "#/components/schemas/ServiceTask"
/tasks/{process_instance_id}/{task_id}: /tasks/{process_instance_id}/{task_guid}:
parameters: parameters:
- name: task_id - name: task_guid
in: path in: path
required: true required: true
description: The unique id of an existing process group. description: The unique id of an existing process group.

View File

@ -41,9 +41,6 @@ from spiffworkflow_backend.models.process_instance_report import (
) # noqa: F401 ) # noqa: F401
from spiffworkflow_backend.models.refresh_token import RefreshTokenModel # noqa: F401 from spiffworkflow_backend.models.refresh_token import RefreshTokenModel # noqa: F401
from spiffworkflow_backend.models.secret_model import SecretModel # noqa: F401 from spiffworkflow_backend.models.secret_model import SecretModel # noqa: F401
from spiffworkflow_backend.models.spiff_step_details import (
SpiffStepDetailsModel,
) # noqa: F401
from spiffworkflow_backend.models.user import UserModel # noqa: F401 from spiffworkflow_backend.models.user import UserModel # noqa: F401
from spiffworkflow_backend.models.group import GroupModel # noqa: F401 from spiffworkflow_backend.models.group import GroupModel # noqa: F401
from spiffworkflow_backend.models.process_instance_metadata import ( from spiffworkflow_backend.models.process_instance_metadata import (

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@ -8,12 +10,17 @@ from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
class BpmnProcessNotFoundError(Exception):
pass
# properties_json attributes: # properties_json attributes:
# "last_task", # guid generated by spiff # "last_task", # guid generated by spiff
# "root", # guid generated by spiff # "root", # guid generated by spiff
# "success", # boolean # "success", # boolean
# "bpmn_messages", # if top-level process # "bpmn_messages", # if top-level process
# "correlations", # if top-level process # "correlations", # if top-level process
@dataclass
class BpmnProcessModel(SpiffworkflowBaseDBModel): class BpmnProcessModel(SpiffworkflowBaseDBModel):
__tablename__ = "bpmn_process" __tablename__ = "bpmn_process"
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(db.Integer, primary_key=True)
@ -24,7 +31,8 @@ class BpmnProcessModel(SpiffworkflowBaseDBModel):
) )
bpmn_process_definition = relationship(BpmnProcessDefinitionModel) bpmn_process_definition = relationship(BpmnProcessDefinitionModel)
parent_process_id: int | None = db.Column(ForeignKey("bpmn_process.id"), nullable=True, index=True) top_level_process_id: int | None = db.Column(ForeignKey("bpmn_process.id"), nullable=True, index=True)
direct_parent_process_id: int | None = db.Column(ForeignKey("bpmn_process.id"), nullable=True, index=True)
properties_json: dict = db.Column(db.JSON, nullable=False) properties_json: dict = db.Column(db.JSON, nullable=False)
json_data_hash: str = db.Column(db.String(255), nullable=False, index=True) json_data_hash: str = db.Column(db.String(255), nullable=False, index=True)

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
@ -10,6 +12,7 @@ from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
# #
# each subprocess will have its own row in this table. # each subprocess will have its own row in this table.
# there is a join table to link them together: bpmn_process_definition_relationship # there is a join table to link them together: bpmn_process_definition_relationship
@dataclass
class BpmnProcessDefinitionModel(SpiffworkflowBaseDBModel): class BpmnProcessDefinitionModel(SpiffworkflowBaseDBModel):
__tablename__ = "bpmn_process_definition" __tablename__ = "bpmn_process_definition"
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(db.Integer, primary_key=True)

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from sqlalchemy import UniqueConstraint from sqlalchemy import UniqueConstraint
@ -10,6 +12,7 @@ from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
@dataclass
class BpmnProcessDefinitionRelationshipModel(SpiffworkflowBaseDBModel): class BpmnProcessDefinitionRelationshipModel(SpiffworkflowBaseDBModel):
__tablename__ = "bpmn_process_definition_relationship" __tablename__ = "bpmn_process_definition_relationship"
__table_args__ = ( __table_args__ = (

View File

@ -87,6 +87,10 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
"ProcessInstanceMetadataModel", "ProcessInstanceMetadataModel",
cascade="delete", cascade="delete",
) # type: ignore ) # type: ignore
process_instance_queue = relationship(
"ProcessInstanceQueueModel",
cascade="delete",
) # type: ignore
start_in_seconds: int | None = db.Column(db.Integer, index=True) start_in_seconds: int | None = db.Column(db.Integer, index=True)
end_in_seconds: int | None = db.Column(db.Integer, index=True) end_in_seconds: int | None = db.Column(db.Integer, index=True)
@ -96,7 +100,6 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
bpmn_version_control_type: str = db.Column(db.String(50)) bpmn_version_control_type: str = db.Column(db.String(50))
bpmn_version_control_identifier: str = db.Column(db.String(255)) bpmn_version_control_identifier: str = db.Column(db.String(255))
spiff_step: int = db.Column(db.Integer)
bpmn_xml_file_contents: str | None = None bpmn_xml_file_contents: str | None = None
process_model_with_diagram_identifier: str | None = None process_model_with_diagram_identifier: str | None = None
@ -117,7 +120,6 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
"bpmn_xml_file_contents": self.bpmn_xml_file_contents, "bpmn_xml_file_contents": self.bpmn_xml_file_contents,
"bpmn_version_control_identifier": self.bpmn_version_control_identifier, "bpmn_version_control_identifier": self.bpmn_version_control_identifier,
"bpmn_version_control_type": self.bpmn_version_control_type, "bpmn_version_control_type": self.bpmn_version_control_type,
"spiff_step": self.spiff_step,
"process_initiator_username": self.process_initiator.username, "process_initiator_username": self.process_initiator.username,
} }

View File

@ -1,37 +0,0 @@
"""Spiff_step_details."""
from dataclasses import dataclass
from typing import Union
from sqlalchemy import ForeignKey
from sqlalchemy import UniqueConstraint
from sqlalchemy.orm import deferred
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
@dataclass
class SpiffStepDetailsModel(SpiffworkflowBaseDBModel):
"""SpiffStepDetailsModel."""
__tablename__ = "spiff_step_details"
__table_args__ = (UniqueConstraint("process_instance_id", "spiff_step", name="process_instance_id_spiff_step"),)
id: int = db.Column(db.Integer, primary_key=True)
process_instance_id: int = db.Column(
ForeignKey(ProcessInstanceModel.id), nullable=False, index=True # type: ignore
)
spiff_step: int = db.Column(db.Integer, nullable=False)
task_json: dict = deferred(db.Column(db.JSON, nullable=False)) # type: ignore
task_id: str = db.Column(db.String(50), nullable=False)
task_state: str = db.Column(db.String(50), nullable=False)
bpmn_task_identifier: str = db.Column(db.String(255), nullable=False)
delta_json: list = deferred(db.Column(db.JSON)) # type: ignore
start_in_seconds: float = db.Column(db.DECIMAL(17, 6), nullable=False)
# to fix mypy in 3.9 - not sure why syntax like:
# float | None
# works in other dataclass db models
end_in_seconds: Union[float, None] = db.Column(db.DECIMAL(17, 6))

View File

@ -63,9 +63,11 @@ class TaskModel(SpiffworkflowBaseDBModel):
json_data_hash: str = db.Column(db.String(255), nullable=False, index=True) json_data_hash: str = db.Column(db.String(255), nullable=False, index=True)
python_env_data_hash: str = db.Column(db.String(255), nullable=False, index=True) python_env_data_hash: str = db.Column(db.String(255), nullable=False, index=True)
start_in_seconds: float = db.Column(db.DECIMAL(17, 6)) start_in_seconds: Union[float, None] = db.Column(db.DECIMAL(17, 6))
end_in_seconds: Union[float, None] = db.Column(db.DECIMAL(17, 6)) end_in_seconds: Union[float, None] = db.Column(db.DECIMAL(17, 6))
data: Optional[dict] = None
def python_env_data(self) -> dict: def python_env_data(self) -> dict:
return JsonDataModel.find_data_dict_by_hash(self.python_env_data_hash) return JsonDataModel.find_data_dict_by_hash(self.python_env_data_hash)
@ -106,7 +108,6 @@ class Task:
event_definition: Union[dict[str, Any], None] = None, event_definition: Union[dict[str, Any], None] = None,
call_activity_process_identifier: Optional[str] = None, call_activity_process_identifier: Optional[str] = None,
calling_subprocess_task_id: Optional[str] = None, calling_subprocess_task_id: Optional[str] = None,
task_spiff_step: Optional[int] = None,
): ):
"""__init__.""" """__init__."""
self.id = id self.id = id
@ -121,7 +122,6 @@ class Task:
self.event_definition = event_definition self.event_definition = event_definition
self.call_activity_process_identifier = call_activity_process_identifier self.call_activity_process_identifier = call_activity_process_identifier
self.calling_subprocess_task_id = calling_subprocess_task_id self.calling_subprocess_task_id = calling_subprocess_task_id
self.task_spiff_step = task_spiff_step
self.data = data self.data = data
if self.data is None: if self.data is None:
@ -179,7 +179,6 @@ class Task:
"event_definition": self.event_definition, "event_definition": self.event_definition,
"call_activity_process_identifier": self.call_activity_process_identifier, "call_activity_process_identifier": self.call_activity_process_identifier,
"calling_subprocess_task_id": self.calling_subprocess_task_id, "calling_subprocess_task_id": self.calling_subprocess_task_id,
"task_spiff_step": self.task_spiff_step,
} }
@classmethod @classmethod

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from sqlalchemy import UniqueConstraint from sqlalchemy import UniqueConstraint
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@ -11,6 +13,7 @@ from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
@dataclass
class TaskDefinitionModel(SpiffworkflowBaseDBModel): class TaskDefinitionModel(SpiffworkflowBaseDBModel):
__tablename__ = "task_definition" __tablename__ = "task_definition"
__table_args__ = ( __table_args__ = (

View File

@ -16,15 +16,9 @@ from spiffworkflow_backend.exceptions.api_error import ApiError
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.db import db
from spiffworkflow_backend.models.json_data import JsonDataModel
from spiffworkflow_backend.models.principal import PrincipalModel from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema
from spiffworkflow_backend.models.process_instance import (
ProcessInstanceTaskDataCannotBeUpdatedError,
)
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType
from spiffworkflow_backend.models.process_instance_file_data import ( from spiffworkflow_backend.models.process_instance_file_data import (
ProcessInstanceFileDataModel, ProcessInstanceFileDataModel,
) )
@ -38,7 +32,6 @@ from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor, ProcessInstanceProcessor,
) )
from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.task_service import TaskService
process_api_blueprint = Blueprint("process_api", __name__) process_api_blueprint = Blueprint("process_api", __name__)
@ -169,60 +162,6 @@ def github_webhook_receive(body: Dict) -> Response:
return Response(json.dumps({"git_pull": result}), status=200, mimetype="application/json") return Response(json.dumps({"git_pull": result}), status=200, mimetype="application/json")
def task_data_update(
process_instance_id: str,
modified_process_model_identifier: str,
task_id: str,
body: Dict,
) -> Response:
"""Update task data."""
process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first()
if process_instance:
if process_instance.status != "suspended":
raise ProcessInstanceTaskDataCannotBeUpdatedError(
"The process instance needs to be suspended to update the task-data."
f" It is currently: {process_instance.status}"
)
task_model = TaskModel.query.filter_by(guid=task_id).first()
if task_model is None:
raise ApiError(
error_code="update_task_data_error",
message=f"Could not find Task: {task_id} in Instance: {process_instance_id}.",
)
if "new_task_data" in body:
new_task_data_str: str = body["new_task_data"]
new_task_data_dict = json.loads(new_task_data_str)
json_data_dict = TaskService.update_task_data_on_task_model(
task_model, new_task_data_dict, "json_data_hash"
)
if json_data_dict is not None:
json_data = JsonDataModel(**json_data_dict)
db.session.add(json_data)
ProcessInstanceProcessor.add_event_to_process_instance(
process_instance, ProcessInstanceEventType.task_data_edited.value, task_guid=task_id
)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
raise ApiError(
error_code="update_task_data_error",
message=f"Could not update the Instance. Original error is {e}",
) from e
else:
raise ApiError(
error_code="update_task_data_error",
message=f"Could not update task data for Instance: {process_instance_id}, and Task: {task_id}.",
)
return Response(
json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
status=200,
mimetype="application/json",
)
def _get_required_parameter_or_raise(parameter: str, post_body: dict[str, Any]) -> Any: def _get_required_parameter_or_raise(parameter: str, post_body: dict[str, Any]) -> Any:
"""Get_required_parameter_or_raise.""" """Get_required_parameter_or_raise."""
return_value = None return_value = None
@ -263,30 +202,6 @@ def send_bpmn_event(
) )
def manual_complete_task(
modified_process_model_identifier: str,
process_instance_id: str,
task_id: str,
body: Dict,
) -> Response:
"""Mark a task complete without executing it."""
execute = body.get("execute", True)
process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first()
if process_instance:
processor = ProcessInstanceProcessor(process_instance)
processor.manual_complete_task(task_id, execute)
else:
raise ApiError(
error_code="complete_task",
message=f"Could not complete Task {task_id} in Instance {process_instance_id}",
)
return Response(
json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
status=200,
mimetype="application/json",
)
def _commit_and_push_to_git(message: str) -> None: def _commit_and_push_to_git(message: str) -> None:
"""Commit_and_push_to_git.""" """Commit_and_push_to_git."""
if current_app.config["SPIFFWORKFLOW_BACKEND_GIT_COMMIT_ON_SAVE"]: if current_app.config["SPIFFWORKFLOW_BACKEND_GIT_COMMIT_ON_SAVE"]:

View File

@ -4,7 +4,6 @@ import json
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import Optional from typing import Optional
from uuid import UUID
import flask.wrappers import flask.wrappers
from flask import current_app from flask import current_app
@ -13,12 +12,12 @@ from flask import jsonify
from flask import make_response from flask import make_response
from flask import request from flask import request
from flask.wrappers import Response from flask.wrappers import Response
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.task import TaskState
from sqlalchemy import and_ from sqlalchemy import and_
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy.orm import aliased
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel
from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.human_task import HumanTaskModel from spiffworkflow_backend.models.human_task import HumanTaskModel
@ -42,8 +41,6 @@ from spiffworkflow_backend.models.process_instance_report import (
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.spec_reference import SpecReferenceNotFoundError from spiffworkflow_backend.models.spec_reference import SpecReferenceNotFoundError
from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel
from spiffworkflow_backend.models.task import Task
from spiffworkflow_backend.models.task import TaskModel from spiffworkflow_backend.models.task import TaskModel
from spiffworkflow_backend.models.task_definition import TaskDefinitionModel from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
@ -85,6 +82,7 @@ from spiffworkflow_backend.services.process_instance_service import (
) )
from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.spec_file_service import SpecFileService
from spiffworkflow_backend.services.task_service import TaskService
def process_instance_create( def process_instance_create(
@ -449,7 +447,6 @@ def process_instance_delete(
# (Pdb) db.session.delete # (Pdb) db.session.delete
# <bound method delete of <sqlalchemy.orm.scoping.scoped_session object at 0x103eaab30>> # <bound method delete of <sqlalchemy.orm.scoping.scoped_session object at 0x103eaab30>>
db.session.query(SpiffStepDetailsModel).filter_by(process_instance_id=process_instance.id).delete()
db.session.query(ProcessInstanceQueueModel).filter_by(process_instance_id=process_instance.id).delete() db.session.query(ProcessInstanceQueueModel).filter_by(process_instance_id=process_instance.id).delete()
db.session.delete(process_instance) db.session.delete(process_instance)
db.session.commit() db.session.commit()
@ -556,142 +553,157 @@ def process_instance_report_show(
def process_instance_task_list_without_task_data_for_me( def process_instance_task_list_without_task_data_for_me(
modified_process_model_identifier: str, modified_process_model_identifier: str,
process_instance_id: int, process_instance_id: int,
all_tasks: bool = False,
spiff_step: int = 0,
most_recent_tasks_only: bool = False, most_recent_tasks_only: bool = False,
bpmn_process_guid: Optional[str] = None,
to_task_guid: Optional[str] = None,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_instance_task_list_without_task_data_for_me.""" """Process_instance_task_list_without_task_data_for_me."""
process_instance = _find_process_instance_for_me_or_raise(process_instance_id) process_instance = _find_process_instance_for_me_or_raise(process_instance_id)
return process_instance_task_list( return process_instance_task_list(
modified_process_model_identifier, _modified_process_model_identifier=modified_process_model_identifier,
process_instance, process_instance=process_instance,
all_tasks, most_recent_tasks_only=most_recent_tasks_only,
spiff_step, bpmn_process_guid=bpmn_process_guid,
most_recent_tasks_only, to_task_guid=to_task_guid,
) )
def process_instance_task_list_without_task_data( def process_instance_task_list_without_task_data(
modified_process_model_identifier: str, modified_process_model_identifier: str,
process_instance_id: int, process_instance_id: int,
all_tasks: bool = False,
spiff_step: int = 0,
most_recent_tasks_only: bool = False, most_recent_tasks_only: bool = False,
bpmn_process_guid: Optional[str] = None,
to_task_guid: Optional[str] = None,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_instance_task_list_without_task_data.""" """Process_instance_task_list_without_task_data."""
process_instance = _find_process_instance_by_id_or_raise(process_instance_id) process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
return process_instance_task_list( return process_instance_task_list(
modified_process_model_identifier, _modified_process_model_identifier=modified_process_model_identifier,
process_instance, process_instance=process_instance,
all_tasks, most_recent_tasks_only=most_recent_tasks_only,
spiff_step, bpmn_process_guid=bpmn_process_guid,
most_recent_tasks_only, to_task_guid=to_task_guid,
) )
def process_instance_task_list( def process_instance_task_list(
_modified_process_model_identifier: str, _modified_process_model_identifier: str,
process_instance: ProcessInstanceModel, process_instance: ProcessInstanceModel,
all_tasks: bool = False, bpmn_process_guid: Optional[str] = None,
spiff_step: int = 0, to_task_guid: Optional[str] = None,
most_recent_tasks_only: bool = False, most_recent_tasks_only: bool = False,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_instance_task_list.""" """Process_instance_task_list."""
step_detail_query = db.session.query(SpiffStepDetailsModel).filter( bpmn_process_ids = []
SpiffStepDetailsModel.process_instance_id == process_instance.id, if bpmn_process_guid:
bpmn_process = BpmnProcessModel.query.filter_by(guid=bpmn_process_guid).first()
bpmn_processes = TaskService.bpmn_process_and_descendants([bpmn_process])
bpmn_process_ids = [p.id for p in bpmn_processes]
task_model_query = db.session.query(TaskModel).filter(
TaskModel.process_instance_id == process_instance.id,
) )
if spiff_step > 0: to_task_model: Optional[TaskModel] = None
step_detail_query = step_detail_query.filter(SpiffStepDetailsModel.spiff_step <= spiff_step) task_models_of_parent_bpmn_processes_guids: list[str] = []
if to_task_guid is not None:
step_details = step_detail_query.all() to_task_model = TaskModel.query.filter_by(guid=to_task_guid, process_instance_id=process_instance.id).first()
if to_task_model is None:
processor = ProcessInstanceProcessor(process_instance) raise ApiError(
full_bpmn_process_dict = processor.full_bpmn_process_dict error_code="task_not_found",
tasks = full_bpmn_process_dict["tasks"] message=f"Cannot find a task with guid '{to_task_guid}' for process instance '{process_instance.id}'",
subprocesses = full_bpmn_process_dict["subprocesses"] status_code=400,
steps_by_id = {step_detail.task_id: step_detail for step_detail in step_details}
def restore_task(spiff_task: dict[str, Any], step_ended: float) -> None:
if spiff_task["last_state_change"] > step_ended:
spiff_task["state"] = Task.task_state_name_to_int("FUTURE")
spiff_task["data"] = {}
if spiff_step > 0:
last_change = step_details[-1].end_in_seconds or 0
for spiff_task in tasks.values():
restore_task(spiff_task, last_change)
for subprocess in subprocesses.values():
for spiff_task in subprocess["tasks"].values():
restore_task(spiff_task, last_change)
bpmn_process_instance = ProcessInstanceProcessor._serializer.workflow_from_dict(full_bpmn_process_dict)
if spiff_step > 0:
bpmn_process_instance.complete_task_from_id(UUID(step_details[-1].task_id))
for subprocess_id, subprocess in bpmn_process_instance.subprocesses.items():
if not subprocess.is_completed():
task = bpmn_process_instance.get_task(subprocess_id)
task._set_state(TaskState.WAITING)
spiff_tasks = None
if all_tasks:
spiff_tasks = bpmn_process_instance.get_tasks(TaskState.ANY_MASK)
else:
spiff_tasks = processor.get_all_user_tasks()
(
subprocesses_by_child_task_ids,
task_typename_by_task_id,
) = processor.get_subprocesses_by_child_task_ids()
processor.get_highest_level_calling_subprocesses_by_child_task_ids(
subprocesses_by_child_task_ids, task_typename_by_task_id
) )
spiff_tasks_to_process = spiff_tasks if to_task_model.state != "COMPLETED":
if most_recent_tasks_only: # TODO: find a better term for viewing at task state
spiff_tasks_by_process_id_and_task_name: dict[str, SpiffTask] = {} raise ApiError(
current_tasks = {} error_code="task_cannot_be_viewed_at",
for spiff_task in spiff_tasks_to_process: message=(
row_id = f"{spiff_task.task_spec._wf_spec.name}:{spiff_task.task_spec.name}" f"Desired task with guid '{to_task_guid}' for process instance '{process_instance.id}' was never"
if spiff_task.state in [TaskState.READY, TaskState.WAITING]: " completed and therefore cannot be viewed at."
current_tasks[row_id] = spiff_task ),
if ( status_code=400,
row_id not in spiff_tasks_by_process_id_and_task_name
or spiff_task.state > spiff_tasks_by_process_id_and_task_name[row_id].state
):
spiff_tasks_by_process_id_and_task_name[row_id] = spiff_task
spiff_tasks_by_process_id_and_task_name.update(current_tasks)
spiff_tasks_to_process = spiff_tasks_by_process_id_and_task_name.values()
response = []
for spiff_task in spiff_tasks_to_process:
task_spiff_step: Optional[int] = None
if str(spiff_task.id) in steps_by_id:
task_spiff_step = steps_by_id[str(spiff_task.id)].spiff_step
calling_subprocess_task_id = subprocesses_by_child_task_ids.get(str(spiff_task.id), None)
task = ProcessInstanceService.spiff_task_to_api_task(
processor,
spiff_task,
calling_subprocess_task_id=calling_subprocess_task_id,
task_spiff_step=task_spiff_step,
) )
if task.state in ["MAYBE", "LIKELY"]:
task.state = "FUTURE"
response.append(task)
return make_response(jsonify(response), 200) _parent_bpmn_processes, task_models_of_parent_bpmn_processes = (
TaskService.task_models_of_parent_bpmn_processes(to_task_model)
)
task_models_of_parent_bpmn_processes_guids = [p.guid for p in task_models_of_parent_bpmn_processes if p.guid]
task_model_query = task_model_query.filter(
or_(
TaskModel.end_in_seconds <= to_task_model.end_in_seconds, # type: ignore
TaskModel.guid.in_(task_models_of_parent_bpmn_processes_guids), # type: ignore
)
)
bpmn_process_alias = aliased(BpmnProcessModel)
direct_parent_bpmn_process_alias = aliased(BpmnProcessModel)
direct_parent_bpmn_process_definition_alias = aliased(BpmnProcessDefinitionModel)
task_model_query = (
task_model_query.order_by(TaskModel.id.desc()) # type: ignore
.join(TaskDefinitionModel, TaskDefinitionModel.id == TaskModel.task_definition_id)
.join(bpmn_process_alias, bpmn_process_alias.id == TaskModel.bpmn_process_id)
.outerjoin(
direct_parent_bpmn_process_alias,
direct_parent_bpmn_process_alias.id == bpmn_process_alias.direct_parent_process_id,
)
.outerjoin(
direct_parent_bpmn_process_definition_alias,
direct_parent_bpmn_process_definition_alias.id
== direct_parent_bpmn_process_alias.bpmn_process_definition_id,
)
.join(
BpmnProcessDefinitionModel, BpmnProcessDefinitionModel.id == TaskDefinitionModel.bpmn_process_definition_id
)
.add_columns(
BpmnProcessDefinitionModel.bpmn_identifier.label("bpmn_process_definition_identifier"), # type: ignore
BpmnProcessDefinitionModel.bpmn_name.label("bpmn_process_definition_name"), # type: ignore
direct_parent_bpmn_process_alias.guid.label("bpmn_process_direct_parent_guid"),
direct_parent_bpmn_process_definition_alias.bpmn_identifier.label(
"bpmn_process_direct_parent_bpmn_identifier"
),
TaskDefinitionModel.bpmn_identifier,
TaskDefinitionModel.bpmn_name,
TaskDefinitionModel.typename,
TaskDefinitionModel.properties_json.label("task_definition_properties_json"), # type: ignore
TaskModel.guid,
TaskModel.state,
TaskModel.end_in_seconds,
TaskModel.start_in_seconds,
)
)
if len(bpmn_process_ids) > 0:
task_model_query = task_model_query.filter(bpmn_process_alias.id.in_(bpmn_process_ids))
task_models = task_model_query.all()
if to_task_model is not None:
task_models_dict = json.loads(current_app.json.dumps(task_models))
for task_model in task_models_dict:
end_in_seconds = float(task_model["end_in_seconds"]) if task_model["end_in_seconds"] is not None else None
if to_task_model.guid == task_model["guid"] and task_model["state"] == "COMPLETED":
TaskService.reset_task_model_dict(task_model, state="READY")
elif (
end_in_seconds is None
or to_task_model.end_in_seconds is None
or to_task_model.end_in_seconds < end_in_seconds
) and task_model["guid"] in task_models_of_parent_bpmn_processes_guids:
TaskService.reset_task_model_dict(task_model, state="WAITING")
return make_response(jsonify(task_models_dict), 200)
return make_response(jsonify(task_models), 200)
def process_instance_reset( def process_instance_reset(
process_instance_id: int, process_instance_id: int,
modified_process_model_identifier: str, modified_process_model_identifier: str,
spiff_step: int = 0, to_task_guid: str,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Reset a process instance to a particular step.""" """Reset a process instance to a particular step."""
process_instance = _find_process_instance_by_id_or_raise(process_instance_id) process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
processor = ProcessInstanceProcessor(process_instance) ProcessInstanceProcessor.reset_process(process_instance, to_task_guid, commit=True)
processor.reset_process(spiff_step)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")

View File

@ -102,7 +102,6 @@ def script_unit_test_run(
"""Script_unit_test_run.""" """Script_unit_test_run."""
# FIXME: We should probably clear this somewhere else but this works # FIXME: We should probably clear this somewhere else but this works
current_app.config["THREAD_LOCAL_DATA"].process_instance_id = None current_app.config["THREAD_LOCAL_DATA"].process_instance_id = None
current_app.config["THREAD_LOCAL_DATA"].spiff_step = None
python_script = _get_required_parameter_or_raise("python_script", body) python_script = _get_required_parameter_or_raise("python_script", body)
input_json = _get_required_parameter_or_raise("input_json", body) input_json = _get_required_parameter_or_raise("input_json", body)

View File

@ -34,10 +34,15 @@ from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.human_task import HumanTaskModel from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel 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.process_instance import ProcessInstanceModelSchema
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_instance import (
ProcessInstanceTaskDataCannotBeUpdatedError,
)
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel
from spiffworkflow_backend.models.task import Task from spiffworkflow_backend.models.task import Task
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.routes.process_api_blueprint import ( from spiffworkflow_backend.routes.process_api_blueprint import (
_find_principal_or_raise, _find_principal_or_raise,
@ -56,6 +61,7 @@ from spiffworkflow_backend.services.process_instance_service import (
) )
from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.spec_file_service import SpecFileService
from spiffworkflow_backend.services.task_service import TaskService
class TaskDataSelectOption(TypedDict): class TaskDataSelectOption(TypedDict):
@ -169,58 +175,99 @@ def task_list_for_my_groups(
def task_data_show( def task_data_show(
modified_process_model_identifier: str, modified_process_model_identifier: str,
process_instance_id: int, process_instance_id: int,
spiff_step: int = 0, task_guid: int = 0,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
process_instance = _find_process_instance_by_id_or_raise(process_instance_id) task_model = TaskModel.query.filter_by(guid=task_guid, process_instance_id=process_instance_id).first()
step_detail = ( if task_model is None:
db.session.query(SpiffStepDetailsModel)
.filter(
SpiffStepDetailsModel.process_instance_id == process_instance.id,
SpiffStepDetailsModel.spiff_step == spiff_step,
)
.first()
)
if step_detail is None:
raise ApiError( raise ApiError(
error_code="spiff_step_for_proces_instance_not_found", error_code="task_not_found",
message="The given spiff step for the given process instance could not be found.", message=f"Cannot find a task with guid '{task_guid}' for process instance '{process_instance_id}'",
status_code=400, status_code=400,
) )
task_model.data = task_model.json_data()
return make_response(jsonify(task_model), 200)
def task_data_update(
process_instance_id: str,
modified_process_model_identifier: str,
task_guid: str,
body: Dict,
) -> Response:
"""Update task data."""
process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first()
if process_instance:
if process_instance.status != "suspended":
raise ProcessInstanceTaskDataCannotBeUpdatedError(
"The process instance needs to be suspended to update the task-data."
f" It is currently: {process_instance.status}"
)
task_model = TaskModel.query.filter_by(guid=task_guid).first()
if task_model is None:
raise ApiError(
error_code="update_task_data_error",
message=f"Could not find Task: {task_guid} in Instance: {process_instance_id}.",
)
if "new_task_data" in body:
new_task_data_str: str = body["new_task_data"]
new_task_data_dict = json.loads(new_task_data_str)
json_data_dict = TaskService.update_task_data_on_task_model(
task_model, new_task_data_dict, "json_data_hash"
)
if json_data_dict is not None:
TaskService.insert_or_update_json_data_records({json_data_dict["hash"]: json_data_dict})
# json_data = JsonDataModel(**json_data_dict)
# db.session.add(json_data)
ProcessInstanceProcessor.add_event_to_process_instance(
process_instance, ProcessInstanceEventType.task_data_edited.value, task_guid=task_guid
)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
raise ApiError(
error_code="update_task_data_error",
message=f"Could not update the Instance. Original error is {e}",
) from e
else:
raise ApiError(
error_code="update_task_data_error",
message=f"Could not update task data for Instance: {process_instance_id}, and Task: {task_guid}.",
)
return Response(
json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
status=200,
mimetype="application/json",
)
def manual_complete_task(
modified_process_model_identifier: str,
process_instance_id: str,
task_guid: str,
body: Dict,
) -> Response:
"""Mark a task complete without executing it."""
execute = body.get("execute", True)
process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first()
if process_instance:
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
spiff_task = processor.__class__.get_task_by_bpmn_identifier( processor.manual_complete_task(task_guid, execute)
step_detail.bpmn_task_identifier, processor.bpmn_process_instance else:
raise ApiError(
error_code="complete_task",
message=f"Could not complete Task {task_guid} in Instance {process_instance_id}",
) )
task_data = step_detail.task_json["task_data"] | step_detail.task_json["python_env"] return Response(
task = ProcessInstanceService.spiff_task_to_api_task( json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
processor, status=200,
spiff_task, mimetype="application/json",
task_spiff_step=spiff_step,
) )
task.data = task_data
return make_response(jsonify(task), 200)
def _munge_form_ui_schema_based_on_hidden_fields_in_task_data(task: Task) -> None: def task_show(process_instance_id: int, task_guid: str) -> flask.wrappers.Response:
if task.form_ui_schema is None:
task.form_ui_schema = {}
if task.data and "form_ui_hidden_fields" in task.data:
hidden_fields = task.data["form_ui_hidden_fields"]
for hidden_field in hidden_fields:
hidden_field_parts = hidden_field.split(".")
relevant_depth_of_ui_schema = task.form_ui_schema
for ii, hidden_field_part in enumerate(hidden_field_parts):
if hidden_field_part not in relevant_depth_of_ui_schema:
relevant_depth_of_ui_schema[hidden_field_part] = {}
relevant_depth_of_ui_schema = relevant_depth_of_ui_schema[hidden_field_part]
if len(hidden_field_parts) == ii + 1:
relevant_depth_of_ui_schema["ui:widget"] = "hidden"
def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response:
"""Task_show.""" """Task_show."""
process_instance = _find_process_instance_by_id_or_raise(process_instance_id) process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
@ -235,12 +282,12 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
process_instance.process_model_identifier, process_instance.process_model_identifier,
) )
_find_human_task_or_raise(process_instance_id, task_id) _find_human_task_or_raise(process_instance_id, task_guid)
form_schema_file_name = "" form_schema_file_name = ""
form_ui_schema_file_name = "" form_ui_schema_file_name = ""
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
spiff_task = _get_spiff_task_from_process_instance(task_id, process_instance, processor=processor) spiff_task = _get_spiff_task_from_process_instance(task_guid, process_instance, processor=processor)
extensions = spiff_task.task_spec.extensions extensions = spiff_task.task_spec.extensions
if "properties" in extensions: if "properties" in extensions:
@ -273,7 +320,8 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
ApiError( ApiError(
error_code="missing_form_file", error_code="missing_form_file",
message=( message=(
f"Cannot find a form file for process_instance_id: {process_instance_id}, task_id: {task_id}" f"Cannot find a form file for process_instance_id: {process_instance_id}, task_guid:"
f" {task_guid}"
), ),
status_code=400, status_code=400,
) )
@ -340,7 +388,7 @@ def process_data_show(
def task_submit_shared( def task_submit_shared(
process_instance_id: int, process_instance_id: int,
task_id: str, task_guid: str,
body: Dict[str, Any], body: Dict[str, Any],
terminate_loop: bool = False, terminate_loop: bool = False,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
@ -357,7 +405,7 @@ def task_submit_shared(
) )
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
spiff_task = _get_spiff_task_from_process_instance(task_id, process_instance, processor=processor) spiff_task = _get_spiff_task_from_process_instance(task_guid, process_instance, processor=processor)
AuthorizationService.assert_user_can_complete_spiff_task(process_instance.id, spiff_task, principal.user) AuthorizationService.assert_user_can_complete_spiff_task(process_instance.id, spiff_task, principal.user)
if spiff_task.state != TaskState.READY: if spiff_task.state != TaskState.READY:
@ -374,7 +422,7 @@ def task_submit_shared(
human_task = _find_human_task_or_raise( human_task = _find_human_task_or_raise(
process_instance_id=process_instance_id, process_instance_id=process_instance_id,
task_id=task_id, task_guid=task_guid,
only_tasks_that_can_be_completed=True, only_tasks_that_can_be_completed=True,
) )
@ -419,13 +467,13 @@ def task_submit_shared(
def task_submit( def task_submit(
process_instance_id: int, process_instance_id: int,
task_id: str, task_guid: str,
body: Dict[str, Any], body: Dict[str, Any],
terminate_loop: bool = False, terminate_loop: bool = False,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Task_submit_user_data.""" """Task_submit_user_data."""
with sentry_sdk.start_span(op="controller_action", description="tasks_controller.task_submit"): with sentry_sdk.start_span(op="controller_action", description="tasks_controller.task_submit"):
return task_submit_shared(process_instance_id, task_id, body, terminate_loop) return task_submit_shared(process_instance_id, task_guid, body, terminate_loop)
def _get_tasks( def _get_tasks(
@ -580,14 +628,14 @@ def _render_jinja_template(unprocessed_template: str, spiff_task: SpiffTask) ->
def _get_spiff_task_from_process_instance( def _get_spiff_task_from_process_instance(
task_id: str, task_guid: str,
process_instance: ProcessInstanceModel, process_instance: ProcessInstanceModel,
processor: Union[ProcessInstanceProcessor, None] = None, processor: Union[ProcessInstanceProcessor, None] = None,
) -> SpiffTask: ) -> SpiffTask:
"""Get_spiff_task_from_process_instance.""" """Get_spiff_task_from_process_instance."""
if processor is None: if processor is None:
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
task_uuid = uuid.UUID(task_id) task_uuid = uuid.UUID(task_guid)
spiff_task = processor.bpmn_process_instance.get_task(task_uuid) spiff_task = processor.bpmn_process_instance.get_task(task_uuid)
if spiff_task is None: if spiff_task is None:
@ -679,15 +727,15 @@ def _get_potential_owner_usernames(assigned_user: AliasedClass) -> Any:
def _find_human_task_or_raise( def _find_human_task_or_raise(
process_instance_id: int, process_instance_id: int,
task_id: str, task_guid: str,
only_tasks_that_can_be_completed: bool = False, only_tasks_that_can_be_completed: bool = False,
) -> HumanTaskModel: ) -> HumanTaskModel:
if only_tasks_that_can_be_completed: if only_tasks_that_can_be_completed:
human_task_query = HumanTaskModel.query.filter_by( human_task_query = HumanTaskModel.query.filter_by(
process_instance_id=process_instance_id, task_id=task_id, completed=False process_instance_id=process_instance_id, task_id=task_guid, completed=False
) )
else: else:
human_task_query = HumanTaskModel.query.filter_by(process_instance_id=process_instance_id, task_id=task_id) human_task_query = HumanTaskModel.query.filter_by(process_instance_id=process_instance_id, task_id=task_guid)
human_task: HumanTaskModel = human_task_query.first() human_task: HumanTaskModel = human_task_query.first()
if human_task is None: if human_task is None:
@ -695,10 +743,27 @@ def _find_human_task_or_raise(
ApiError( ApiError(
error_code="no_human_task", error_code="no_human_task",
message=( message=(
f"Cannot find a task to complete for task id '{task_id}' and" f"Cannot find a task to complete for task id '{task_guid}' and"
f" process instance {process_instance_id}." f" process instance {process_instance_id}."
), ),
status_code=500, status_code=500,
) )
) )
return human_task return human_task
def _munge_form_ui_schema_based_on_hidden_fields_in_task_data(task: Task) -> None:
if task.form_ui_schema is None:
task.form_ui_schema = {}
if task.data and "form_ui_hidden_fields" in task.data:
hidden_fields = task.data["form_ui_hidden_fields"]
for hidden_field in hidden_fields:
hidden_field_parts = hidden_field.split(".")
relevant_depth_of_ui_schema = task.form_ui_schema
for ii, hidden_field_part in enumerate(hidden_field_parts):
if hidden_field_part not in relevant_depth_of_ui_schema:
relevant_depth_of_ui_schema[hidden_field_part] = {}
relevant_depth_of_ui_schema = relevant_depth_of_ui_schema[hidden_field_part]
if len(hidden_field_parts) == ii + 1:
relevant_depth_of_ui_schema["ui:widget"] = "hidden"

View File

@ -9,7 +9,6 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.script_attributes_context import ( from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext, ScriptAttributesContext,
) )
from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel
from spiffworkflow_backend.scripts.script import Script from spiffworkflow_backend.scripts.script import Script
@ -43,14 +42,6 @@ class DeleteProcessInstancesWithCriteria(Script):
rows_affected = len(results) rows_affected = len(results)
if rows_affected > 0: if rows_affected > 0:
ids_to_delete = list(map(lambda r: r.id, results)) # type: ignore
step_details = SpiffStepDetailsModel.query.filter(
SpiffStepDetailsModel.process_instance_id.in_(ids_to_delete) # type: ignore
).all()
for deletion in step_details:
db.session.delete(deletion)
for deletion in results: for deletion in results:
db.session.delete(deletion) db.session.delete(deletion)
db.session.commit() db.session.commit()

View File

@ -6,7 +6,6 @@ import sys
from typing import Any from typing import Any
from typing import Optional from typing import Optional
from flask import g
from flask.app import Flask from flask.app import Flask
@ -88,28 +87,6 @@ class JsonFormatter(logging.Formatter):
return json.dumps(message_dict, default=str) return json.dumps(message_dict, default=str)
class SpiffFilter(logging.Filter):
"""SpiffFilter."""
def __init__(self, app: Flask):
"""__init__."""
self.app = app
super().__init__()
def filter(self, record: logging.LogRecord) -> bool:
"""Filter."""
tld = self.app.config["THREAD_LOCAL_DATA"]
process_instance_id = ""
if hasattr(tld, "process_instance_id"):
process_instance_id = tld.process_instance_id
setattr(record, "process_instance_id", process_instance_id) # noqa: B010
if hasattr(tld, "spiff_step"):
setattr(record, "spiff_step", tld.spiff_step) # noqa: 8010
if hasattr(g, "user") and g.user:
setattr(record, "current_user_id", g.user.id) # noqa: B010
return True
def setup_logger(app: Flask) -> None: def setup_logger(app: Flask) -> None:
"""Setup_logger.""" """Setup_logger."""
upper_log_level_string = app.config["SPIFFWORKFLOW_BACKEND_LOG_LEVEL"].upper() upper_log_level_string = app.config["SPIFFWORKFLOW_BACKEND_LOG_LEVEL"].upper()

View File

@ -39,7 +39,6 @@ from SpiffWorkflow.bpmn.serializer.task_spec import ( # type: ignore
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # type: ignore from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # type: ignore
from SpiffWorkflow.bpmn.specs.BpmnProcessSpec import BpmnProcessSpec # type: ignore from SpiffWorkflow.bpmn.specs.BpmnProcessSpec import BpmnProcessSpec # type: ignore
from SpiffWorkflow.bpmn.specs.events.EndEvent import EndEvent # type: ignore from SpiffWorkflow.bpmn.specs.events.EndEvent import EndEvent # type: ignore
from SpiffWorkflow.bpmn.specs.events.event_definitions import CancelEventDefinition # type: ignore
from SpiffWorkflow.bpmn.specs.events.StartEvent import StartEvent # type: ignore from SpiffWorkflow.bpmn.specs.events.StartEvent import StartEvent # type: ignore
from SpiffWorkflow.bpmn.specs.SubWorkflowTask import SubWorkflowTask # type: ignore from SpiffWorkflow.bpmn.specs.SubWorkflowTask import SubWorkflowTask # type: ignore
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore
@ -84,16 +83,13 @@ from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext, ScriptAttributesContext,
) )
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel from spiffworkflow_backend.models.task import TaskModel
from spiffworkflow_backend.models.task import TaskModel # noqa: F401 from spiffworkflow_backend.models.task import TaskNotFoundError
from spiffworkflow_backend.models.task_definition import TaskDefinitionModel from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.scripts.script import Script from spiffworkflow_backend.scripts.script import Script
from spiffworkflow_backend.services.custom_parser import MyCustomParser from spiffworkflow_backend.services.custom_parser import MyCustomParser
from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.process_instance_lock_service import (
ProcessInstanceLockService,
)
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsAlreadyLockedError from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsAlreadyLockedError
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceQueueService from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceQueueService
from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.process_model_service import ProcessModelService
@ -104,9 +100,6 @@ from spiffworkflow_backend.services.user_service import UserService
from spiffworkflow_backend.services.workflow_execution_service import ( from spiffworkflow_backend.services.workflow_execution_service import (
execution_strategy_named, execution_strategy_named,
) )
from spiffworkflow_backend.services.workflow_execution_service import (
StepDetailLoggingDelegate,
)
from spiffworkflow_backend.services.workflow_execution_service import ( from spiffworkflow_backend.services.workflow_execution_service import (
TaskModelSavingDelegate, TaskModelSavingDelegate,
) )
@ -150,14 +143,6 @@ class MissingProcessInfoError(Exception):
"""MissingProcessInfoError.""" """MissingProcessInfoError."""
class SpiffStepDetailIsMissingError(Exception):
pass
class TaskNotFoundError(Exception):
pass
class BoxedTaskDataBasedScriptEngineEnvironment(BoxedTaskDataEnvironment): # type: ignore class BoxedTaskDataBasedScriptEngineEnvironment(BoxedTaskDataEnvironment): # type: ignore
def __init__(self, environment_globals: Dict[str, Any]): def __init__(self, environment_globals: Dict[str, Any]):
"""BoxedTaskDataBasedScriptEngineEnvironment.""" """BoxedTaskDataBasedScriptEngineEnvironment."""
@ -436,7 +421,6 @@ class ProcessInstanceProcessor:
"""Create a Workflow Processor based on the serialized information available in the process_instance model.""" """Create a Workflow Processor based on the serialized information available in the process_instance model."""
tld = current_app.config["THREAD_LOCAL_DATA"] tld = current_app.config["THREAD_LOCAL_DATA"]
tld.process_instance_id = process_instance_model.id tld.process_instance_id = process_instance_model.id
tld.spiff_step = process_instance_model.spiff_step
# we want this to be the fully qualified path to the process model including all group subcomponents # we want this to be the fully qualified path to the process model including all group subcomponents
current_app.config["THREAD_LOCAL_DATA"].process_model_identifier = ( current_app.config["THREAD_LOCAL_DATA"].process_model_identifier = (
@ -687,7 +671,7 @@ class ProcessInstanceProcessor:
single_bpmn_process_dict = cls._get_bpmn_process_dict(bpmn_process, get_tasks=True) single_bpmn_process_dict = cls._get_bpmn_process_dict(bpmn_process, get_tasks=True)
spiff_bpmn_process_dict.update(single_bpmn_process_dict) spiff_bpmn_process_dict.update(single_bpmn_process_dict)
bpmn_subprocesses = BpmnProcessModel.query.filter_by(parent_process_id=bpmn_process.id).all() bpmn_subprocesses = BpmnProcessModel.query.filter_by(top_level_process_id=bpmn_process.id).all()
bpmn_subprocess_id_to_guid_mappings = {} bpmn_subprocess_id_to_guid_mappings = {}
for bpmn_subprocess in bpmn_subprocesses: for bpmn_subprocess in bpmn_subprocesses:
bpmn_subprocess_id_to_guid_mappings[bpmn_subprocess.id] = bpmn_subprocess.guid bpmn_subprocess_id_to_guid_mappings[bpmn_subprocess.id] = bpmn_subprocess.guid
@ -817,37 +801,6 @@ class ProcessInstanceProcessor:
"lane_assignment_id": lane_assignment_id, "lane_assignment_id": lane_assignment_id,
} }
def spiff_step_details_mapping(
self,
spiff_task: Optional[SpiffTask] = None,
start_in_seconds: Optional[float] = None,
end_in_seconds: Optional[float] = None,
) -> dict:
"""SaveSpiffStepDetails."""
if spiff_task is None:
# TODO: safer to pass in task vs use last task?
spiff_task = self.bpmn_process_instance.last_task
if spiff_task is None:
return {}
# it's only None when we're starting a human task (it's not complete yet)
if start_in_seconds is None:
start_in_seconds = time.time()
task_json = self.get_task_dict_from_spiff_task(spiff_task)
return {
"process_instance_id": self.process_instance_model.id,
"spiff_step": self.process_instance_model.spiff_step or 1,
"task_json": task_json,
"task_id": str(spiff_task.id),
"task_state": spiff_task.get_state_name(),
"bpmn_task_identifier": spiff_task.task_spec.name,
"start_in_seconds": start_in_seconds,
"end_in_seconds": end_in_seconds,
}
def extract_metadata(self, process_model_info: ProcessModelInfo) -> None: def extract_metadata(self, process_model_info: ProcessModelInfo) -> None:
"""Extract_metadata.""" """Extract_metadata."""
metadata_extraction_paths = process_model_info.metadata_extraction_paths metadata_extraction_paths = process_model_info.metadata_extraction_paths
@ -1185,14 +1138,7 @@ class ProcessInstanceProcessor:
human_task_user = HumanTaskUserModel(user_id=potential_owner_id, human_task=human_task) human_task_user = HumanTaskUserModel(user_id=potential_owner_id, human_task=human_task)
db.session.add(human_task_user) db.session.add(human_task_user)
self.increment_spiff_step()
spiff_step_detail_mapping = self.spiff_step_details_mapping(
spiff_task=ready_or_waiting_task, start_in_seconds=time.time()
)
spiff_step_detail = SpiffStepDetailsModel(**spiff_step_detail_mapping)
db.session.add(spiff_step_detail)
db.session.commit() db.session.commit()
# self.log_spiff_step_details(spiff_step_detail_mapping)
if len(human_tasks) > 0: if len(human_tasks) > 0:
for at in human_tasks: for at in human_tasks:
@ -1223,18 +1169,10 @@ class ProcessInstanceProcessor:
# TODO: do_engine_steps without a lock # TODO: do_engine_steps without a lock
self.do_engine_steps(save=True) self.do_engine_steps(save=True)
def add_step(self, step: Union[dict, None] = None) -> None:
"""Add a spiff step."""
if step is None:
step = self.spiff_step_details_mapping()
spiff_step_detail = SpiffStepDetailsModel(**step)
db.session.add(spiff_step_detail)
db.session.commit()
# self.log_spiff_step_details(step)
def manual_complete_task(self, task_id: str, execute: bool) -> None: def manual_complete_task(self, task_id: str, execute: bool) -> None:
"""Mark the task complete optionally executing it.""" """Mark the task complete optionally executing it."""
spiff_tasks_updated = {} spiff_tasks_updated = {}
start_in_seconds = time.time()
spiff_task = self.bpmn_process_instance.get_task(UUID(task_id)) spiff_task = self.bpmn_process_instance.get_task(UUID(task_id))
event_type = ProcessInstanceEventType.task_skipped.value event_type = ProcessInstanceEventType.task_skipped.value
if execute: if execute:
@ -1267,6 +1205,8 @@ class ProcessInstanceProcessor:
spiff_task.workflow.last_task = spiff_task spiff_task.workflow.last_task = spiff_task
spiff_tasks_updated[spiff_task.id] = spiff_task spiff_tasks_updated[spiff_task.id] = spiff_task
end_in_seconds = time.time()
if isinstance(spiff_task.task_spec, EndEvent): if isinstance(spiff_task.task_spec, EndEvent):
for task in self.bpmn_process_instance.get_tasks(TaskState.DEFINITE_MASK, workflow=spiff_task.workflow): for task in self.bpmn_process_instance.get_tasks(TaskState.DEFINITE_MASK, workflow=spiff_task.workflow):
task.complete() task.complete()
@ -1279,9 +1219,6 @@ class ProcessInstanceProcessor:
task.complete() task.complete()
spiff_tasks_updated[task.id] = task spiff_tasks_updated[task.id] = task
self.increment_spiff_step()
self.add_step()
for updated_spiff_task in spiff_tasks_updated.values(): for updated_spiff_task in spiff_tasks_updated.values():
bpmn_process, task_model, new_task_models, new_json_data_dicts = ( bpmn_process, task_model, new_task_models, new_json_data_dicts = (
TaskService.find_or_create_task_model_from_spiff_task( TaskService.find_or_create_task_model_from_spiff_task(
@ -1303,6 +1240,11 @@ class ProcessInstanceProcessor:
if bpmn_process_json_data is not None: if bpmn_process_json_data is not None:
new_json_data_dicts[bpmn_process_json_data["hash"]] = bpmn_process_json_data new_json_data_dicts[bpmn_process_json_data["hash"]] = bpmn_process_json_data
# spiff_task should be the main task we are completing and only it should get the timestamps
if task_model.guid == str(spiff_task.id):
task_model.start_in_seconds = start_in_seconds
task_model.end_in_seconds = end_in_seconds
new_task_models[task_model.guid] = task_model new_task_models[task_model.guid] = task_model
db.session.bulk_save_objects(new_task_models.values()) db.session.bulk_save_objects(new_task_models.values())
TaskService.insert_or_update_json_data_records(new_json_data_dicts) TaskService.insert_or_update_json_data_records(new_json_data_dicts)
@ -1312,48 +1254,118 @@ class ProcessInstanceProcessor:
# Saving the workflow seems to reset the status # Saving the workflow seems to reset the status
self.suspend() self.suspend()
def reset_process(self, spiff_step: int) -> None: # FIXME: this currently cannot work for multi-instance tasks and loopback. It can somewhat for not those
# if we can properly handling resetting children tasks. Right now if we set them all to FUTURE then
# they never get picked up by spiff and processed. The process instance just stops after the to_task_guid
# and marks itself complete without processing any of the children.
@classmethod
def reset_process(
cls, process_instance: ProcessInstanceModel, to_task_guid: str, commit: Optional[bool] = False
) -> None:
"""Reset a process to an earlier state.""" """Reset a process to an earlier state."""
spiff_logger = logging.getLogger("spiff") raise Exception("This feature to reset a process instance to a given task is currently unavaiable")
spiff_logger.info( # cls.add_event_to_process_instance(
f"Process reset from step {spiff_step}", # process_instance, ProcessInstanceEventType.process_instance_rewound_to_task.value, task_guid=to_task_guid
extra=self.bpmn_process_instance.log_info(), # )
) #
# to_task_model = TaskModel.query.filter_by(guid=to_task_guid, process_instance_id=process_instance.id).first()
step_detail = ( # if to_task_model is None:
db.session.query(SpiffStepDetailsModel) # raise TaskNotFoundError(
.filter( # f"Cannot find a task with guid '{to_task_guid}' for process instance '{process_instance.id}'"
SpiffStepDetailsModel.process_instance_id == self.process_instance_model.id, # )
SpiffStepDetailsModel.spiff_step == spiff_step, #
) # parent_bpmn_processes, task_models_of_parent_bpmn_processes = TaskService.task_models_of_parent_bpmn_processes(
.first() # to_task_model
) # )
if step_detail is not None: # [p.guid for p in task_models_of_parent_bpmn_processes if p.guid]
self.increment_spiff_step() # [p.id for p in parent_bpmn_processes]
self.add_step( # tasks_to_update_query = db.session.query(TaskModel).filter(
{ # and_(
"process_instance_id": self.process_instance_model.id, # or_(
"spiff_step": self.process_instance_model.spiff_step or 1, # TaskModel.end_in_seconds > to_task_model.end_in_seconds,
"task_json": step_detail.task_json, # TaskModel.end_in_seconds.is_(None), # type: ignore
"timestamp": round(time.time()), # ),
} # TaskModel.process_instance_id == process_instance.id,
) # # TaskModel.bpmn_process_id.in_(parent_bpmn_processes_ids), # type: ignore
# )
dct = self._serializer.workflow_to_dict(self.bpmn_process_instance) # )
dct["tasks"] = step_detail.task_json["tasks"] # tasks_to_update = tasks_to_update_query.all()
dct["subprocesses"] = step_detail.task_json["subprocesses"] #
self.bpmn_process_instance = self._serializer.workflow_from_dict(dct) # # run all queries before making changes to task_model
# if commit:
# Cascade does not seems to work on filters, only directly through the session # # tasks_to_delete_query = db.session.query(TaskModel).filter(
tasks = self.bpmn_process_instance.get_tasks(TaskState.NOT_FINISHED_MASK) # # and_(
rows = HumanTaskModel.query.filter( # # or_(
HumanTaskModel.task_id.in_(str(t.id) for t in tasks) # type: ignore # # TaskModel.end_in_seconds > to_task_model.end_in_seconds,
).all() # # TaskModel.end_in_seconds.is_not(None), # type: ignore
for row in rows: # # ),
db.session.delete(row) # # TaskModel.process_instance_id == process_instance.id,
# # TaskModel.guid.not_in(task_models_of_parent_bpmn_processes_guids), # type: ignore
self.save() # # TaskModel.bpmn_process_id.not_in(parent_bpmn_processes_ids), # type: ignore
self.suspend() # # )
# # )
# #
# # tasks_to_delete = tasks_to_delete_query.all()
# #
# # # delete any later tasks from to_task_model and delete bpmn processes that may be
# # # link directly to one of those tasks.
# # tasks_to_delete_guids = [t.guid for t in tasks_to_delete]
# # tasks_to_delete_ids = [t.id for t in tasks_to_delete]
# # bpmn_processes_to_delete = BpmnProcessModel.query.filter(
# # BpmnProcessModel.guid.in_(tasks_to_delete_guids) # type: ignore
# # ).order_by(BpmnProcessModel.id.desc()).all()
# # human_tasks_to_delete = HumanTaskModel.query.filter(
# # HumanTaskModel.task_model_id.in_(tasks_to_delete_ids) # type: ignore
# # ).all()
# #
# #
# # import pdb; pdb.set_trace()
# # # ensure the correct order for foreign keys
# # for human_task_to_delete in human_tasks_to_delete:
# # db.session.delete(human_task_to_delete)
# # db.session.commit()
# # for task_to_delete in tasks_to_delete:
# # db.session.delete(task_to_delete)
# # db.session.commit()
# # for bpmn_process_to_delete in bpmn_processes_to_delete:
# # db.session.delete(bpmn_process_to_delete)
# # db.session.commit()
#
# related_human_task = HumanTaskModel.query.filter_by(task_model_id=to_task_model.id).first()
# if related_human_task is not None:
# db.session.delete(related_human_task)
#
# tasks_to_update_ids = [t.id for t in tasks_to_update]
# human_tasks_to_delete = HumanTaskModel.query.filter(
# HumanTaskModel.task_model_id.in_(tasks_to_update_ids) # type: ignore
# ).all()
# for human_task_to_delete in human_tasks_to_delete:
# db.session.delete(human_task_to_delete)
# db.session.commit()
#
# for task_to_update in tasks_to_update:
# TaskService.reset_task_model(task_to_update, state="FUTURE", commit=commit)
#
# parent_task_model = TaskModel.query.filter_by(guid=to_task_model.properties_json["parent"]).first()
# if parent_task_model is None:
# raise TaskNotFoundError(
# f"Cannot find a task with guid '{to_task_guid}' for process instance '{process_instance.id}'"
# )
#
# TaskService.reset_task_model(
# to_task_model,
# state="READY",
# json_data_hash=parent_task_model.json_data_hash,
# python_env_data_hash=parent_task_model.python_env_data_hash,
# commit=commit,
# )
# for task_model in task_models_of_parent_bpmn_processes:
# TaskService.reset_task_model(task_model, state="WAITING", commit=commit)
#
# if commit:
# processor = ProcessInstanceProcessor(process_instance)
# processor.save()
# processor.suspend()
@staticmethod @staticmethod
def get_parser() -> MyCustomParser: def get_parser() -> MyCustomParser:
@ -1591,31 +1603,15 @@ class ProcessInstanceProcessor:
db.session.add(message_instance) db.session.add(message_instance)
db.session.commit() db.session.commit()
def increment_spiff_step(self) -> None:
"""Spiff_step++."""
spiff_step = self.process_instance_model.spiff_step or 0
spiff_step += 1
self.process_instance_model.spiff_step = spiff_step
current_app.config["THREAD_LOCAL_DATA"].spiff_step = spiff_step
db.session.add(self.process_instance_model)
def do_engine_steps( def do_engine_steps(
self, self,
exit_at: None = None, exit_at: None = None,
save: bool = False, save: bool = False,
execution_strategy_name: Optional[str] = None, execution_strategy_name: Optional[str] = None,
) -> None: ) -> None:
# NOTE: To avoid saving spiff step details, just comment out this function and the step_delegate and
# set the TaskModelSavingDelegate's secondary_engine_step_delegate to None.
def spiff_step_details_mapping_builder(task: SpiffTask, start: float, end: float) -> dict:
self._script_engine.environment.revise_state_with_task_data(task)
return self.spiff_step_details_mapping(task, start, end)
self._add_bpmn_process_definitions() self._add_bpmn_process_definitions()
step_delegate = StepDetailLoggingDelegate(self.increment_spiff_step, spiff_step_details_mapping_builder)
task_model_delegate = TaskModelSavingDelegate( task_model_delegate = TaskModelSavingDelegate(
secondary_engine_step_delegate=step_delegate,
serializer=self._serializer, serializer=self._serializer,
process_instance=self.process_instance_model, process_instance=self.process_instance_model,
bpmn_definition_to_task_definitions_mappings=self.bpmn_definition_to_task_definitions_mappings, bpmn_definition_to_task_definitions_mappings=self.bpmn_definition_to_task_definitions_mappings,
@ -1643,31 +1639,6 @@ class ProcessInstanceProcessor:
): ):
self._script_engine.failing_spiff_task = None self._script_engine.failing_spiff_task = None
# log the spiff step details so we know what is processing the process
# instance when a human task has a timer event.
def log_spiff_step_details(self, step_details: Any) -> None:
if ProcessInstanceLockService.has_lock(self.process_instance_model.id):
locked_by = ProcessInstanceLockService.locked_by()
message = f"ADDING SPIFF BULK STEP DETAILS: {locked_by}: {step_details}"
current_app.logger.debug(message)
def cancel_notify(self) -> None:
"""Cancel_notify."""
self.__cancel_notify(self.bpmn_process_instance)
@staticmethod
def __cancel_notify(bpmn_process_instance: BpmnWorkflow) -> None:
"""__cancel_notify."""
try:
# A little hackly, but make the bpmn_process_instance catch a cancel event.
bpmn_process_instance.signal("cancel") # generate a cancel signal.
bpmn_process_instance.catch(CancelEventDefinition())
# Due to this being static, can't save granular step details in this case
# TODO: do_engine_steps without a lock
bpmn_process_instance.do_engine_steps()
except WorkflowTaskException as we:
raise ApiError.from_workflow_exception("task_error", str(we), we) from we
@classmethod @classmethod
def get_tasks_with_data(cls, bpmn_process_instance: BpmnWorkflow) -> List[SpiffTask]: def get_tasks_with_data(cls, bpmn_process_instance: BpmnWorkflow) -> List[SpiffTask]:
return [task for task in bpmn_process_instance.get_tasks(TaskState.FINISHED_MASK) if len(task.data) > 0] return [task for task in bpmn_process_instance.get_tasks(TaskState.FINISHED_MASK) if len(task.data) > 0]
@ -1816,28 +1787,6 @@ class ProcessInstanceProcessor:
human_task.task_status = spiff_task.get_state_name() human_task.task_status = spiff_task.get_state_name()
db.session.add(human_task) db.session.add(human_task)
# FIXME: remove when we switch over to using tasks only
details_model = (
SpiffStepDetailsModel.query.filter_by(
process_instance_id=self.process_instance_model.id,
task_id=str(spiff_task.id),
task_state="READY",
)
.order_by(SpiffStepDetailsModel.id.desc()) # type: ignore
.first()
)
if details_model is None:
raise SpiffStepDetailIsMissingError(
"Cannot find a ready spiff_step_detail entry for process instance"
f" {self.process_instance_model.id} and task_id is {spiff_task.id}"
)
details_model.task_state = spiff_task.get_state_name()
details_model.end_in_seconds = time.time()
details_model.task_json = self.get_task_dict_from_spiff_task(spiff_task)
db.session.add(details_model)
# #######
json_data_dict_list = TaskService.update_task_model(task_model, spiff_task, self._serializer) json_data_dict_list = TaskService.update_task_model(task_model, spiff_task, self._serializer)
for json_data_dict in json_data_dict_list: for json_data_dict in json_data_dict_list:
if json_data_dict is not None: if json_data_dict is not None:

View File

@ -404,7 +404,6 @@ class ProcessInstanceService:
spiff_task: SpiffTask, spiff_task: SpiffTask,
add_docs_and_forms: bool = False, add_docs_and_forms: bool = False,
calling_subprocess_task_id: Optional[str] = None, calling_subprocess_task_id: Optional[str] = None,
task_spiff_step: Optional[int] = None,
) -> Task: ) -> Task:
"""Spiff_task_to_api_task.""" """Spiff_task_to_api_task."""
task_type = spiff_task.task_spec.spec_type task_type = spiff_task.task_spec.spec_type
@ -443,7 +442,6 @@ class ProcessInstanceService:
event_definition=serialized_task_spec.get("event_definition"), event_definition=serialized_task_spec.get("event_definition"),
call_activity_process_identifier=call_activity_process_identifier, call_activity_process_identifier=call_activity_process_identifier,
calling_subprocess_task_id=calling_subprocess_task_id, calling_subprocess_task_id=calling_subprocess_task_id,
task_spiff_step=task_spiff_step,
) )
return task return task

View File

@ -9,11 +9,13 @@ from flask import current_app
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflow # type: ignore from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer
from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from SpiffWorkflow.task import TaskState
from SpiffWorkflow.task import TaskStateNames from SpiffWorkflow.task import TaskStateNames
from sqlalchemy.dialects.mysql import insert as mysql_insert from sqlalchemy.dialects.mysql import insert as mysql_insert
from sqlalchemy.dialects.postgresql import insert as postgres_insert from sqlalchemy.dialects.postgresql import insert as postgres_insert
from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel
from spiffworkflow_backend.models.bpmn_process import BpmnProcessNotFoundError
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.json_data import JsonDataModel # noqa: F401 from spiffworkflow_backend.models.json_data import JsonDataModel # noqa: F401
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
@ -144,7 +146,7 @@ class TaskService:
bpmn_process, new_task_models, new_json_data_dicts = cls.add_bpmn_process( bpmn_process, new_task_models, new_json_data_dicts = cls.add_bpmn_process(
bpmn_process_dict=serializer.workflow_to_dict(subprocess), bpmn_process_dict=serializer.workflow_to_dict(subprocess),
process_instance=process_instance, process_instance=process_instance,
bpmn_process_parent=process_instance.bpmn_process, top_level_process=process_instance.bpmn_process,
bpmn_process_guid=subprocess_guid, bpmn_process_guid=subprocess_guid,
bpmn_definition_to_task_definitions_mappings=bpmn_definition_to_task_definitions_mappings, bpmn_definition_to_task_definitions_mappings=bpmn_definition_to_task_definitions_mappings,
spiff_workflow=spiff_workflow, spiff_workflow=spiff_workflow,
@ -160,7 +162,7 @@ class TaskService:
bpmn_definition_to_task_definitions_mappings: dict, bpmn_definition_to_task_definitions_mappings: dict,
spiff_workflow: BpmnWorkflow, spiff_workflow: BpmnWorkflow,
serializer: BpmnWorkflowSerializer, serializer: BpmnWorkflowSerializer,
bpmn_process_parent: Optional[BpmnProcessModel] = None, top_level_process: Optional[BpmnProcessModel] = None,
bpmn_process_guid: Optional[str] = None, bpmn_process_guid: Optional[str] = None,
) -> Tuple[BpmnProcessModel, dict[str, TaskModel], dict[str, JsonDataDict]]: ) -> Tuple[BpmnProcessModel, dict[str, TaskModel], dict[str, JsonDataDict]]:
"""This creates and adds a bpmn_process to the Db session. """This creates and adds a bpmn_process to the Db session.
@ -182,9 +184,9 @@ class TaskService:
new_json_data_dicts: dict[str, JsonDataDict] = {} new_json_data_dicts: dict[str, JsonDataDict] = {}
bpmn_process = None bpmn_process = None
if bpmn_process_parent is not None: if top_level_process is not None:
bpmn_process = BpmnProcessModel.query.filter_by( bpmn_process = BpmnProcessModel.query.filter_by(
parent_process_id=bpmn_process_parent.id, guid=bpmn_process_guid top_level_process_id=top_level_process.id, guid=bpmn_process_guid
).first() ).first()
elif process_instance.bpmn_process_id is not None: elif process_instance.bpmn_process_id is not None:
bpmn_process = process_instance.bpmn_process bpmn_process = process_instance.bpmn_process
@ -194,6 +196,32 @@ class TaskService:
bpmn_process_is_new = True bpmn_process_is_new = True
bpmn_process = BpmnProcessModel(guid=bpmn_process_guid) bpmn_process = BpmnProcessModel(guid=bpmn_process_guid)
bpmn_process_definition = bpmn_definition_to_task_definitions_mappings[spiff_workflow.spec.name][
"bpmn_process_definition"
]
bpmn_process.bpmn_process_definition = bpmn_process_definition
if top_level_process is not None:
subprocesses = spiff_workflow._get_outermost_workflow().subprocesses
direct_bpmn_process_parent = top_level_process
for subprocess_guid, subprocess in subprocesses.items():
if subprocess == spiff_workflow.outer_workflow:
direct_bpmn_process_parent = BpmnProcessModel.query.filter_by(
guid=str(subprocess_guid)
).first()
if direct_bpmn_process_parent is None:
raise BpmnProcessNotFoundError(
f"Could not find bpmn process with guid: {str(subprocess_guid)} "
f"while searching for direct parent process of {bpmn_process_guid}."
)
if direct_bpmn_process_parent is None:
raise BpmnProcessNotFoundError(
f"Could not find a direct bpmn process parent for guid: {bpmn_process_guid}"
)
bpmn_process.direct_parent_process_id = direct_bpmn_process_parent.id
# Point the root id to the Start task instead of the Root task # Point the root id to the Start task instead of the Root task
# since we are ignoring the Root task. # since we are ignoring the Root task.
for task_id, task_properties in tasks.items(): for task_id, task_properties in tasks.items():
@ -206,15 +234,10 @@ class TaskService:
if bpmn_process_json_data is not None: if bpmn_process_json_data is not None:
new_json_data_dicts[bpmn_process_json_data["hash"]] = bpmn_process_json_data new_json_data_dicts[bpmn_process_json_data["hash"]] = bpmn_process_json_data
if bpmn_process_parent is None: if top_level_process is None:
process_instance.bpmn_process = bpmn_process process_instance.bpmn_process = bpmn_process
elif bpmn_process.parent_process_id is None: elif bpmn_process.top_level_process_id is None:
bpmn_process.parent_process_id = bpmn_process_parent.id bpmn_process.top_level_process_id = top_level_process.id
bpmn_process_definition = bpmn_definition_to_task_definitions_mappings[spiff_workflow.spec.name][
"bpmn_process_definition"
]
bpmn_process.bpmn_process_definition = bpmn_process_definition
# Since we bulk insert tasks later we need to add the bpmn_process to the session # Since we bulk insert tasks later we need to add the bpmn_process to the session
# to ensure we have an id. # to ensure we have an id.
@ -285,6 +308,76 @@ class TaskService:
setattr(task_model, task_model_data_column, task_data_hash) setattr(task_model, task_model_data_column, task_data_hash)
return json_data_dict return json_data_dict
@classmethod
def bpmn_process_and_descendants(cls, bpmn_processes: list[BpmnProcessModel]) -> list[BpmnProcessModel]:
bpmn_process_ids = [p.id for p in bpmn_processes]
direct_children = BpmnProcessModel.query.filter(
BpmnProcessModel.direct_parent_process_id.in_(bpmn_process_ids) # type: ignore
).all()
if len(direct_children) > 0:
return bpmn_processes + cls.bpmn_process_and_descendants(direct_children)
return bpmn_processes
@classmethod
def task_models_of_parent_bpmn_processes(
cls, task_model: TaskModel
) -> Tuple[list[BpmnProcessModel], list[TaskModel]]:
bpmn_process = task_model.bpmn_process
task_models: list[TaskModel] = []
bpmn_processes: list[BpmnProcessModel] = [bpmn_process]
if bpmn_process.guid is not None:
parent_task_model = TaskModel.query.filter_by(guid=bpmn_process.guid).first()
if parent_task_model is not None:
b, t = cls.task_models_of_parent_bpmn_processes(parent_task_model)
return (bpmn_processes + b, [parent_task_model] + t)
return (bpmn_processes, task_models)
@classmethod
def reset_task_model_dict(
cls,
task_model: dict,
state: str,
) -> None:
task_model["state"] = state
task_model["start_in_seconds"] = None
task_model["end_in_seconds"] = None
@classmethod
def reset_task_model(
cls,
task_model: TaskModel,
state: str,
commit: Optional[bool] = True,
json_data_hash: Optional[str] = None,
python_env_data_hash: Optional[str] = None,
) -> None:
if json_data_hash is None:
cls.update_task_data_on_task_model(task_model, {}, "json_data_hash")
else:
task_model.json_data_hash = json_data_hash
if python_env_data_hash is None:
cls.update_task_data_on_task_model(task_model, {}, "python_env_data")
else:
task_model.python_env_data_hash = python_env_data_hash
new_properties_json = task_model.properties_json
task_model.state = state
task_model.start_in_seconds = None
task_model.end_in_seconds = None
if commit:
db.session.add(task_model)
db.session.commit()
new_properties_json["state"] = getattr(TaskState, state)
task_model.properties_json = new_properties_json
if commit:
# if we commit the properties json at the same time as the other items
# the json gets reset for some reason.
db.session.add(task_model)
db.session.commit()
@classmethod @classmethod
def _create_task( def _create_task(
cls, cls,

View File

@ -1,7 +1,6 @@
import logging import logging
import time import time
from typing import Callable from typing import Callable
from typing import List
from typing import Optional from typing import Optional
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # type: ignore from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # type: ignore
@ -19,7 +18,6 @@ from spiffworkflow_backend.models.message_instance_correlation import (
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType
from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel
from spiffworkflow_backend.models.task import TaskModel # noqa: F401 from spiffworkflow_backend.models.task import TaskModel # noqa: F401
from spiffworkflow_backend.services.assertion_service import safe_assertion from spiffworkflow_backend.services.assertion_service import safe_assertion
from spiffworkflow_backend.services.process_instance_lock_service import ( from spiffworkflow_backend.services.process_instance_lock_service import (
@ -45,10 +43,6 @@ class EngineStepDelegate:
pass pass
SpiffStepIncrementer = Callable[[], None]
SpiffStepDetailsMappingBuilder = Callable[[SpiffTask, float, float], dict]
class TaskModelSavingDelegate(EngineStepDelegate): class TaskModelSavingDelegate(EngineStepDelegate):
"""Engine step delegate that takes care of saving a task model to the database. """Engine step delegate that takes care of saving a task model to the database.
@ -107,6 +101,8 @@ class TaskModelSavingDelegate(EngineStepDelegate):
def after_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> None: def after_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> None:
if self._should_update_task_model(): if self._should_update_task_model():
# TODO: also include children of the last task processed. This may help with task resets
# if we have to set their states to FUTURE.
# excludes FUTURE and COMPLETED. the others were required to get PP1 to go to completion. # excludes FUTURE and COMPLETED. the others were required to get PP1 to go to completion.
for waiting_spiff_task in bpmn_process_instance.get_tasks( for waiting_spiff_task in bpmn_process_instance.get_tasks(
TaskState.WAITING | TaskState.CANCELLED | TaskState.READY | TaskState.MAYBE | TaskState.LIKELY TaskState.WAITING | TaskState.CANCELLED | TaskState.READY | TaskState.MAYBE | TaskState.LIKELY
@ -165,58 +161,6 @@ class TaskModelSavingDelegate(EngineStepDelegate):
return task_model return task_model
class StepDetailLoggingDelegate(EngineStepDelegate):
"""Engine step delegate that takes care of logging spiff step details.
This separates the concerns of step execution and step logging.
"""
def __init__(
self,
increment_spiff_step: SpiffStepIncrementer,
spiff_step_details_mapping: SpiffStepDetailsMappingBuilder,
):
"""__init__."""
self.increment_spiff_step = increment_spiff_step
self.spiff_step_details_mapping = spiff_step_details_mapping
self.step_details: List[dict] = []
self.current_task_start_in_seconds = 0.0
self.tasks_to_log = {
"BPMN Task",
"Script Task",
"Service Task",
"Default Start Event",
"Exclusive Gateway",
"Call Activity",
# "End Join",
"End Event",
"Default Throwing Event",
"Subprocess",
"Transactional Subprocess",
}
def should_log(self, spiff_task: SpiffTask) -> bool:
return spiff_task.task_spec.spec_type in self.tasks_to_log and not spiff_task.task_spec.name.endswith(
".EndJoin"
)
def will_complete_task(self, spiff_task: SpiffTask) -> None:
if self.should_log(spiff_task):
self.current_task_start_in_seconds = time.time()
self.increment_spiff_step()
def did_complete_task(self, spiff_task: SpiffTask) -> None:
if self.should_log(spiff_task):
self.step_details.append(
self.spiff_step_details_mapping(spiff_task, self.current_task_start_in_seconds, time.time())
)
def save(self, _bpmn_process_instance: BpmnWorkflow, commit: bool = True) -> None:
db.session.bulk_insert_mappings(SpiffStepDetailsModel, self.step_details)
if commit:
db.session.commit()
class ExecutionStrategy: class ExecutionStrategy:
"""Interface of sorts for a concrete execution strategy.""" """Interface of sorts for a concrete execution strategy."""

View File

@ -22,11 +22,10 @@
<bpmn:script>set_in_top_level_script = 1</bpmn:script> <bpmn:script>set_in_top_level_script = 1</bpmn:script>
</bpmn:scriptTask> </bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1fktmf7" sourceRef="top_level_script" targetRef="manual_task" /> <bpmn:sequenceFlow id="Flow_1fktmf7" sourceRef="top_level_script" targetRef="manual_task" />
<bpmn:sequenceFlow id="Flow_1i7syph" sourceRef="top_level_subprocess" targetRef="top_level_call_activity" />
<bpmn:sequenceFlow id="Flow_09gjylo" sourceRef="manual_task" targetRef="top_level_subprocess" /> <bpmn:sequenceFlow id="Flow_09gjylo" sourceRef="manual_task" targetRef="top_level_subprocess" />
<bpmn:subProcess id="top_level_subprocess"> <bpmn:subProcess id="top_level_subprocess">
<bpmn:incoming>Flow_09gjylo</bpmn:incoming> <bpmn:incoming>Flow_09gjylo</bpmn:incoming>
<bpmn:outgoing>Flow_1i7syph</bpmn:outgoing> <bpmn:outgoing>Flow_0yxus36</bpmn:outgoing>
<bpmn:startEvent id="Event_0g7txdo"> <bpmn:startEvent id="Event_0g7txdo">
<bpmn:outgoing>Flow_00k1tii</bpmn:outgoing> <bpmn:outgoing>Flow_00k1tii</bpmn:outgoing>
</bpmn:startEvent> </bpmn:startEvent>
@ -48,7 +47,7 @@ except:
</bpmn:scriptTask> </bpmn:scriptTask>
</bpmn:subProcess> </bpmn:subProcess>
<bpmn:callActivity id="top_level_call_activity" calledElement="test_process_to_call"> <bpmn:callActivity id="top_level_call_activity" calledElement="test_process_to_call">
<bpmn:incoming>Flow_1i7syph</bpmn:incoming> <bpmn:incoming>Flow_0yxus36</bpmn:incoming>
<bpmn:outgoing>Flow_187mcqe</bpmn:outgoing> <bpmn:outgoing>Flow_187mcqe</bpmn:outgoing>
</bpmn:callActivity> </bpmn:callActivity>
<bpmn:exclusiveGateway id="Gateway_0p8naw0" default="Flow_1t9ywmr"> <bpmn:exclusiveGateway id="Gateway_0p8naw0" default="Flow_1t9ywmr">
@ -67,6 +66,7 @@ except:
<bpmn:script>set_top_level_process_script_after_gate = 1</bpmn:script> <bpmn:script>set_top_level_process_script_after_gate = 1</bpmn:script>
</bpmn:scriptTask> </bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1t9ywmr" sourceRef="Gateway_0p8naw0" targetRef="manual_task" /> <bpmn:sequenceFlow id="Flow_1t9ywmr" sourceRef="Gateway_0p8naw0" targetRef="manual_task" />
<bpmn:sequenceFlow id="Flow_0yxus36" sourceRef="top_level_subprocess" targetRef="top_level_call_activity" />
</bpmn:process> </bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="top_level_process"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="top_level_process">
@ -102,10 +102,6 @@ except:
<di:waypoint x="370" y="177" /> <di:waypoint x="370" y="177" />
<di:waypoint x="400" y="177" /> <di:waypoint x="400" y="177" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1i7syph_di" bpmnElement="Flow_1i7syph">
<di:waypoint x="630" y="177" />
<di:waypoint x="680" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_09gjylo_di" bpmnElement="Flow_09gjylo"> <bpmndi:BPMNEdge id="Flow_09gjylo_di" bpmnElement="Flow_09gjylo">
<di:waypoint x="500" y="177" /> <di:waypoint x="500" y="177" />
<di:waypoint x="530" y="177" /> <di:waypoint x="530" y="177" />
@ -128,6 +124,10 @@ except:
<di:waypoint x="450" y="100" /> <di:waypoint x="450" y="100" />
<di:waypoint x="450" y="137" /> <di:waypoint x="450" y="137" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0yxus36_di" bpmnElement="Flow_0yxus36">
<di:waypoint x="630" y="177" />
<di:waypoint x="680" y="177" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane> </bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram> </bpmndi:BPMNDiagram>
<bpmndi:BPMNDiagram id="BPMNDiagram_01cbxj3"> <bpmndi:BPMNDiagram id="BPMNDiagram_01cbxj3">

View File

@ -1,38 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev"> <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="test_process_to_call" name="Test Process To Call" isExecutable="true"> <bpmn:process id="test_process_to_call" name="Test Process To Call" isExecutable="true">
<bpmn:startEvent id="StartEvent_1"> <bpmn:endEvent id="Event_03zsjvn">
<bpmn:outgoing>Flow_06g687y</bpmn:outgoing> <bpmn:incoming>Flow_089aeua</bpmn:incoming>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_06g687y" sourceRef="StartEvent_1" targetRef="test_process_to_call_script" />
<bpmn:endEvent id="Event_1nn875f">
<bpmn:incoming>Flow_01e21r0</bpmn:incoming>
</bpmn:endEvent> </bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_01e21r0" sourceRef="test_process_to_call_script" targetRef="Event_1nn875f" /> <bpmn:startEvent id="Event_0pp84tn">
<bpmn:outgoing>Flow_1qsx5et</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:subProcess id="test_process_to_call_subprocess">
<bpmn:incoming>Flow_1qsx5et</bpmn:incoming>
<bpmn:outgoing>Flow_0zedtvv</bpmn:outgoing>
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0bkk554</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:scriptTask id="test_process_to_call_subprocess_script" name="Test Process To Call Subprocess Script">
<bpmn:incoming>Flow_1cnuh2a</bpmn:incoming>
<bpmn:outgoing>Flow_17hgw9g</bpmn:outgoing>
<bpmn:script>set_in_test_process_to_call_subprocess_script = 1</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="Event_1nn875f">
<bpmn:incoming>Flow_17hgw9g</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_17hgw9g" sourceRef="test_process_to_call_subprocess_script" targetRef="Event_1nn875f" />
<bpmn:sequenceFlow id="Flow_0bkk554" sourceRef="StartEvent_1" targetRef="test_process_to_call_subprocess_subprocess" />
<bpmn:sequenceFlow id="Flow_1cnuh2a" sourceRef="test_process_to_call_subprocess_subprocess" targetRef="test_process_to_call_subprocess_script" />
<bpmn:subProcess id="test_process_to_call_subprocess_subprocess">
<bpmn:incoming>Flow_0bkk554</bpmn:incoming>
<bpmn:outgoing>Flow_1cnuh2a</bpmn:outgoing>
<bpmn:startEvent id="Event_17bk1sd">
<bpmn:outgoing>Flow_1nri60d</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1nri60d" sourceRef="Event_17bk1sd" targetRef="test_process_to_call_subprocess_subprocess_script" />
<bpmn:endEvent id="Event_1sec2vg">
<bpmn:incoming>Flow_1bfzrzu</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1bfzrzu" sourceRef="test_process_to_call_subprocess_subprocess_script" targetRef="Event_1sec2vg" />
<bpmn:scriptTask id="test_process_to_call_subprocess_subprocess_script" name="Test Process To Call Subprocess Subprocess Script">
<bpmn:incoming>Flow_1nri60d</bpmn:incoming>
<bpmn:outgoing>Flow_1bfzrzu</bpmn:outgoing>
<bpmn:script>set_in_test_process_to_call_subprocess_subprocess_script = 1</bpmn:script>
</bpmn:scriptTask>
</bpmn:subProcess>
</bpmn:subProcess>
<bpmn:sequenceFlow id="Flow_1qsx5et" sourceRef="Event_0pp84tn" targetRef="test_process_to_call_subprocess" />
<bpmn:sequenceFlow id="Flow_0zedtvv" sourceRef="test_process_to_call_subprocess" targetRef="test_process_to_call_script" />
<bpmn:sequenceFlow id="Flow_089aeua" sourceRef="test_process_to_call_script" targetRef="Event_03zsjvn" />
<bpmn:scriptTask id="test_process_to_call_script" name="Test Process To Call Script"> <bpmn:scriptTask id="test_process_to_call_script" name="Test Process To Call Script">
<bpmn:incoming>Flow_06g687y</bpmn:incoming> <bpmn:incoming>Flow_0zedtvv</bpmn:incoming>
<bpmn:outgoing>Flow_01e21r0</bpmn:outgoing> <bpmn:outgoing>Flow_089aeua</bpmn:outgoing>
<bpmn:script>set_in_test_process_to_call_script = 1</bpmn:script> <bpmn:script>set_in_test_process_to_call_script = 1</bpmn:script>
</bpmn:scriptTask> </bpmn:scriptTask>
</bpmn:process> </bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="test_process_to_call"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="test_process_to_call">
<bpmndi:BPMNShape id="Event_03zsjvn_di" bpmnElement="Event_03zsjvn">
<dc:Bounds x="612" y="132" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0pp84tn_di" bpmnElement="Event_0pp84tn">
<dc:Bounds x="162.33333333333334" y="132" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_14ywg4w_di" bpmnElement="test_process_to_call_subprocess" isExpanded="false">
<dc:Bounds x="270" y="110" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_17algy3_di" bpmnElement="test_process_to_call_script">
<dc:Bounds x="450" y="110" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1qsx5et_di" bpmnElement="Flow_1qsx5et">
<di:waypoint x="198" y="150" />
<di:waypoint x="270" y="150" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0zedtvv_di" bpmnElement="Flow_0zedtvv">
<di:waypoint x="370" y="150" />
<di:waypoint x="450" y="150" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_089aeua_di" bpmnElement="Flow_089aeua">
<di:waypoint x="550" y="150" />
<di:waypoint x="612" y="150" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
<bpmndi:BPMNDiagram id="BPMNDiagram_1ikntvh">
<bpmndi:BPMNPlane id="BPMNPlane_18euprj" bpmnElement="test_process_to_call_subprocess">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1"> <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" /> <dc:Bounds x="180" y="182" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_059upl6_di" bpmnElement="test_process_to_call_subprocess_script">
<dc:Bounds x="420" y="160" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1nn875f_di" bpmnElement="Event_1nn875f"> <bpmndi:BPMNShape id="Event_1nn875f_di" bpmnElement="Event_1nn875f">
<dc:Bounds x="432" y="159" width="36" height="36" /> <dc:Bounds x="562" y="182" width="36" height="36" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_059upl6_di" bpmnElement="test_process_to_call_script"> <bpmndi:BPMNShape id="Activity_0hixo7p_di" bpmnElement="test_process_to_call_subprocess_subprocess">
<dc:Bounds x="270" y="137" width="100" height="80" /> <dc:Bounds x="270" y="160" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_06g687y_di" bpmnElement="Flow_06g687y"> <bpmndi:BPMNEdge id="Flow_17hgw9g_di" bpmnElement="Flow_17hgw9g">
<di:waypoint x="215" y="177" /> <di:waypoint x="520" y="200" />
<di:waypoint x="270" y="177" /> <di:waypoint x="562" y="200" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_01e21r0_di" bpmnElement="Flow_01e21r0"> <bpmndi:BPMNEdge id="Flow_0bkk554_di" bpmnElement="Flow_0bkk554">
<di:waypoint x="370" y="177" /> <di:waypoint x="216" y="200" />
<di:waypoint x="432" y="177" /> <di:waypoint x="270" y="200" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1cnuh2a_di" bpmnElement="Flow_1cnuh2a">
<di:waypoint x="370" y="200" />
<di:waypoint x="420" y="200" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
<bpmndi:BPMNDiagram id="BPMNDiagram_0x7adrd">
<bpmndi:BPMNPlane id="BPMNPlane_17ay7ws" bpmnElement="test_process_to_call_subprocess_subprocess">
<bpmndi:BPMNShape id="Event_17bk1sd_di" bpmnElement="Event_17bk1sd">
<dc:Bounds x="262" y="172" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1sec2vg_di" bpmnElement="Event_1sec2vg">
<dc:Bounds x="502" y="172" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1kjd6i3_di" bpmnElement="test_process_to_call_subprocess_subprocess_script">
<dc:Bounds x="350" y="150" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1nri60d_di" bpmnElement="Flow_1nri60d">
<di:waypoint x="298" y="190" />
<di:waypoint x="350" y="190" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1bfzrzu_di" bpmnElement="Flow_1bfzrzu">
<di:waypoint x="450" y="190" />
<di:waypoint x="502" y="190" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
</bpmndi:BPMNPlane> </bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram> </bpmndi:BPMNDiagram>

View File

@ -27,7 +27,7 @@
</bpmn:intermediateCatchEvent> </bpmn:intermediateCatchEvent>
<bpmn:sequenceFlow id="Flow_0tzaigt" sourceRef="Gateway_0n53kj7" targetRef="Event_0himdx6" /> <bpmn:sequenceFlow id="Flow_0tzaigt" sourceRef="Gateway_0n53kj7" targetRef="Event_0himdx6" />
<bpmn:sequenceFlow id="Flow_1vld4r2" sourceRef="Gateway_0n53kj7" targetRef="Event_0e4owa3" /> <bpmn:sequenceFlow id="Flow_1vld4r2" sourceRef="Gateway_0n53kj7" targetRef="Event_0e4owa3" />
<bpmn:sequenceFlow id="Flow_13ai5vv" sourceRef="Event_0e4owa3" targetRef="Activity_0uum4kq" /> <bpmn:sequenceFlow id="Flow_13ai5vv" sourceRef="Event_0e4owa3" targetRef="manual_task_one" />
<bpmn:endEvent id="Event_0vmxgb9"> <bpmn:endEvent id="Event_0vmxgb9">
<bpmn:incoming>Flow_1q47ol8</bpmn:incoming> <bpmn:incoming>Flow_1q47ol8</bpmn:incoming>
</bpmn:endEvent> </bpmn:endEvent>
@ -36,7 +36,7 @@
<bpmn:endEvent id="Event_174a838"> <bpmn:endEvent id="Event_174a838">
<bpmn:incoming>Flow_1w3n49n</bpmn:incoming> <bpmn:incoming>Flow_1w3n49n</bpmn:incoming>
</bpmn:endEvent> </bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1vwnf3n" sourceRef="Activity_0uum4kq" targetRef="Event_1ixib8a" /> <bpmn:sequenceFlow id="Flow_1vwnf3n" sourceRef="manual_task_one" targetRef="Event_1ixib8a" />
<bpmn:intermediateCatchEvent id="Event_0e4owa3"> <bpmn:intermediateCatchEvent id="Event_0e4owa3">
<bpmn:incoming>Flow_1vld4r2</bpmn:incoming> <bpmn:incoming>Flow_1vld4r2</bpmn:incoming>
<bpmn:outgoing>Flow_13ai5vv</bpmn:outgoing> <bpmn:outgoing>Flow_13ai5vv</bpmn:outgoing>
@ -44,7 +44,7 @@
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">"PT1H"</bpmn:timeDuration> <bpmn:timeDuration xsi:type="bpmn:tFormalExpression">"PT1H"</bpmn:timeDuration>
</bpmn:timerEventDefinition> </bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent> </bpmn:intermediateCatchEvent>
<bpmn:manualTask id="Activity_0uum4kq" name="Any Task"> <bpmn:manualTask id="manual_task_one" name="Any Task">
<bpmn:extensionElements> <bpmn:extensionElements>
<spiffworkflow:instructionsForEndUser>Click the button.</spiffworkflow:instructionsForEndUser> <spiffworkflow:instructionsForEndUser>Click the button.</spiffworkflow:instructionsForEndUser>
</bpmn:extensionElements> </bpmn:extensionElements>
@ -91,7 +91,7 @@
<bpmndi:BPMNShape id="Event_0e4owa3_di" bpmnElement="Event_0e4owa3"> <bpmndi:BPMNShape id="Event_0e4owa3_di" bpmnElement="Event_0e4owa3">
<dc:Bounds x="392" y="272" width="36" height="36" /> <dc:Bounds x="392" y="272" width="36" height="36" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_104jmxm_di" bpmnElement="Activity_0uum4kq"> <bpmndi:BPMNShape id="Activity_104jmxm_di" bpmnElement="manual_task_one">
<dc:Bounds x="480" y="250" width="100" height="80" /> <dc:Bounds x="480" y="250" width="100" height="80" />
<bpmndi:BPMNLabel /> <bpmndi:BPMNLabel />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>

View File

@ -2619,9 +2619,9 @@ class TestProcessApi(BaseTest):
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
end_task = next(task for task in response.json if task["type"] == "End Event") end_task = next(task for task in response.json if task["bpmn_identifier"] == "Event_174a838")
response = client.get( response = client.get(
f"/v1.0/task-data/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/{end_task['task_spiff_step']}", f"/v1.0/task-data/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/{end_task['guid']}",
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
@ -2688,17 +2688,17 @@ class TestProcessApi(BaseTest):
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/task-info", f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/task-info",
headers=self.logged_in_headers(with_super_admin_user), headers=self.logged_in_headers(with_super_admin_user),
) )
assert len(response.json) == 1 assert len(response.json) == 9
task = response.json[0] human_task = next(task for task in response.json if task["bpmn_identifier"] == "manual_task_one")
response = client.post( response = client.post(
f"/v1.0/task-complete/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/{task['id']}", f"/v1.0/task-complete/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/{human_task['guid']}",
headers=self.logged_in_headers(with_super_admin_user), headers=self.logged_in_headers(with_super_admin_user),
content_type="application/json", content_type="application/json",
data=json.dumps({"execute": False}), data=json.dumps({"execute": False}),
) )
assert response.json["status"] == "suspended" assert response.json["status"] == "suspended"
task_model = TaskModel.query.filter_by(guid=task["id"]).first() task_model = TaskModel.query.filter_by(guid=human_task["guid"]).first()
assert task_model is not None assert task_model is not None
assert task_model.state == "COMPLETED" assert task_model.state == "COMPLETED"
@ -2707,7 +2707,7 @@ class TestProcessApi(BaseTest):
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
assert len(response.json) == 1 assert len(response.json) == 9
def setup_initial_groups_for_move_tests(self, client: FlaskClient, with_super_admin_user: UserModel) -> None: def setup_initial_groups_for_move_tests(self, client: FlaskClient, with_super_admin_user: UserModel) -> None:
"""Setup_initial_groups_for_move_tests.""" """Setup_initial_groups_for_move_tests."""

View File

@ -91,7 +91,6 @@ class TestErrorHandlingService(BaseTest):
# Both send and receive messages should be generated, matched # Both send and receive messages should be generated, matched
# and considered complete. # and considered complete.
messages = db.session.query(MessageInstanceModel).all() messages = db.session.query(MessageInstanceModel).all()
# import pdb; pdb.set_trace()
assert 2 == len(messages) assert 2 == len(messages)
assert "completed" == messages[0].status assert "completed" == messages[0].status
assert "completed" == messages[1].status assert "completed" == messages[1].status

View File

@ -256,6 +256,61 @@ class TestProcessInstanceProcessor(BaseTest):
assert spiff_task is not None assert spiff_task is not None
assert spiff_task.state == TaskState.COMPLETED assert spiff_task.state == TaskState.COMPLETED
# TODO: FIX resetting a process instance to a task
# def test_properly_resets_process_to_given_task(
# self,
# app: Flask,
# client: FlaskClient,
# with_db_and_bpmn_file_cleanup: None,
# with_super_admin_user: UserModel,
# ) -> None:
# self.create_process_group(client, with_super_admin_user, "test_group", "test_group")
# initiator_user = self.find_or_create_user("initiator_user")
# finance_user_three = self.find_or_create_user("testuser3")
# assert initiator_user.principal is not None
# assert finance_user_three.principal is not None
# AuthorizationService.import_permissions_from_yaml_file()
#
# finance_group = GroupModel.query.filter_by(identifier="Finance Team").first()
# assert finance_group is not None
#
# process_model = load_test_spec(
# process_model_id="test_group/manual_task_with_subprocesses",
# process_model_source_directory="manual_task_with_subprocesses",
# )
# process_instance = self.create_process_instance_from_process_model(
# process_model=process_model, user=initiator_user
# )
# processor = ProcessInstanceProcessor(process_instance)
# processor.do_engine_steps(save=True)
# assert len(process_instance.active_human_tasks) == 1
# initial_human_task_id = process_instance.active_human_tasks[0].id
#
# # save again to ensure we go attempt to process the human tasks again
# processor.save()
#
# assert len(process_instance.active_human_tasks) == 1
# assert initial_human_task_id == process_instance.active_human_tasks[0].id
#
# processor = ProcessInstanceProcessor(process_instance)
# human_task_one = process_instance.active_human_tasks[0]
# spiff_manual_task = processor.__class__.get_task_by_bpmn_identifier(
# human_task_one.task_name, processor.bpmn_process_instance
# )
# ProcessInstanceService.complete_form_task(processor, spiff_manual_task, {}, initiator_user, human_task_one)
#
# processor.suspend()
# ProcessInstanceProcessor.reset_process(process_instance, str(spiff_manual_task.id), commit=True)
#
# process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first()
# processor = ProcessInstanceProcessor(process_instance)
# human_task_one = process_instance.active_human_tasks[0]
# spiff_manual_task = processor.bpmn_process_instance.get_task(UUID(human_task_one.task_id))
# ProcessInstanceService.complete_form_task(processor, spiff_manual_task, {}, initiator_user, human_task_one)
# human_task_one = process_instance.active_human_tasks[0]
# spiff_manual_task = processor.bpmn_process_instance.get_task(UUID(human_task_one.task_id))
# ProcessInstanceService.complete_form_task(processor, spiff_manual_task, {}, initiator_user, human_task_one)
def test_properly_saves_tasks_when_running( def test_properly_saves_tasks_when_running(
self, self,
app: Flask, app: Flask,
@ -263,7 +318,6 @@ class TestProcessInstanceProcessor(BaseTest):
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_does_not_recreate_human_tasks_on_multiple_saves."""
self.create_process_group(client, with_super_admin_user, "test_group", "test_group") self.create_process_group(client, with_super_admin_user, "test_group", "test_group")
initiator_user = self.find_or_create_user("initiator_user") initiator_user = self.find_or_create_user("initiator_user")
finance_user_three = self.find_or_create_user("testuser3") finance_user_three = self.find_or_create_user("testuser3")
@ -317,7 +371,11 @@ class TestProcessInstanceProcessor(BaseTest):
} }
third_data_set = { third_data_set = {
**second_data_set, **second_data_set,
**{"set_in_test_process_to_call_script": 1}, **{
"set_in_test_process_to_call_script": 1,
"set_in_test_process_to_call_subprocess_subprocess_script": 1,
"set_in_test_process_to_call_subprocess_script": 1,
},
} }
fourth_data_set = {**third_data_set, **{"a": 1, "we_move_on": True}} fourth_data_set = {**third_data_set, **{"a": 1, "we_move_on": True}}
fifth_data_set = {**fourth_data_set, **{"validate_only": False, "set_top_level_process_script_after_gate": 1}} fifth_data_set = {**fourth_data_set, **{"validate_only": False, "set_top_level_process_script_after_gate": 1}}
@ -326,11 +384,11 @@ class TestProcessInstanceProcessor(BaseTest):
"manual_task": first_data_set, "manual_task": first_data_set,
"top_level_subprocess_script": second_data_set, "top_level_subprocess_script": second_data_set,
"top_level_subprocess": second_data_set, "top_level_subprocess": second_data_set,
"test_process_to_call_script": third_data_set, "test_process_to_call_subprocess_script": third_data_set,
"top_level_call_activity": third_data_set, "top_level_call_activity": third_data_set,
"end_event_of_manual_task_model": third_data_set, "end_event_of_manual_task_model": third_data_set,
"top_level_subprocess_script_second": fourth_data_set, "top_level_subprocess_script_second": fourth_data_set,
"test_process_to_call_script_second": fourth_data_set, "test_process_to_call_subprocess_script_second": fourth_data_set,
} }
spiff_tasks_checked_once: list = [] spiff_tasks_checked_once: list = []
@ -338,10 +396,16 @@ class TestProcessInstanceProcessor(BaseTest):
# TODO: also check task data here from the spiff_task directly to ensure we hydrated spiff correctly # TODO: also check task data here from the spiff_task directly to ensure we hydrated spiff correctly
def assert_spiff_task_is_in_process(spiff_task_identifier: str, bpmn_process_identifier: str) -> None: def assert_spiff_task_is_in_process(spiff_task_identifier: str, bpmn_process_identifier: str) -> None:
if spiff_task.task_spec.name == spiff_task_identifier: if spiff_task.task_spec.name == spiff_task_identifier:
base_failure_message = f"Failed on {bpmn_process_identifier} - {spiff_task_identifier}." expected_task_data_key = spiff_task.task_spec.name
expected_python_env_data = expected_task_data[spiff_task.task_spec.name]
if spiff_task.task_spec.name in spiff_tasks_checked_once: if spiff_task.task_spec.name in spiff_tasks_checked_once:
expected_python_env_data = expected_task_data[f"{spiff_task.task_spec.name}_second"] expected_task_data_key = f"{spiff_task.task_spec.name}_second"
expected_python_env_data = expected_task_data[expected_task_data_key]
base_failure_message = (
f"Failed on {bpmn_process_identifier} - {spiff_task_identifier} - task data key"
f" {expected_task_data_key}."
)
task_model = TaskModel.query.filter_by(guid=str(spiff_task.id)).first() task_model = TaskModel.query.filter_by(guid=str(spiff_task.id)).first()
assert task_model.start_in_seconds is not None assert task_model.start_in_seconds is not None
@ -354,7 +418,8 @@ class TestProcessInstanceProcessor(BaseTest):
assert task_definition.bpmn_process_definition.bpmn_identifier == bpmn_process_identifier assert task_definition.bpmn_process_definition.bpmn_identifier == bpmn_process_identifier
message = ( message = (
f"{base_failure_message} Expected: {expected_python_env_data}. Received: {task_model.json_data()}" f"{base_failure_message} Expected: {sorted(expected_python_env_data)}. Received:"
f" {sorted(task_model.json_data())}"
) )
# TODO: if we split out env data again we will need to use it here instead of json_data # TODO: if we split out env data again we will need to use it here instead of json_data
# assert task_model.python_env_data() == expected_python_env_data, message # assert task_model.python_env_data() == expected_python_env_data, message
@ -365,7 +430,9 @@ class TestProcessInstanceProcessor(BaseTest):
assert len(all_spiff_tasks) > 1 assert len(all_spiff_tasks) > 1
for spiff_task in all_spiff_tasks: for spiff_task in all_spiff_tasks:
assert spiff_task.state == TaskState.COMPLETED assert spiff_task.state == TaskState.COMPLETED
assert_spiff_task_is_in_process("test_process_to_call_script", "test_process_to_call") assert_spiff_task_is_in_process(
"test_process_to_call_subprocess_script", "test_process_to_call_subprocess"
)
assert_spiff_task_is_in_process("top_level_subprocess_script", "top_level_subprocess") assert_spiff_task_is_in_process("top_level_subprocess_script", "top_level_subprocess")
assert_spiff_task_is_in_process("top_level_script", "top_level_process") assert_spiff_task_is_in_process("top_level_script", "top_level_process")
@ -378,6 +445,23 @@ class TestProcessInstanceProcessor(BaseTest):
assert bpmn_process_definition.bpmn_identifier == "test_process_to_call" assert bpmn_process_definition.bpmn_identifier == "test_process_to_call"
assert bpmn_process_definition.bpmn_name == "Test Process To Call" assert bpmn_process_definition.bpmn_name == "Test Process To Call"
# Check that the direct parent of the called activity subprocess task is the
# name of the process that was called from the activity.
if spiff_task.task_spec.name == "test_process_to_call_subprocess_script":
task_model = TaskModel.query.filter_by(guid=str(spiff_task.id)).first()
assert task_model is not None
bpmn_process = task_model.bpmn_process
assert bpmn_process is not None
bpmn_process_definition = bpmn_process.bpmn_process_definition
assert bpmn_process_definition is not None
assert bpmn_process_definition.bpmn_identifier == "test_process_to_call_subprocess"
assert bpmn_process.direct_parent_process_id is not None
direct_parent_process = BpmnProcessModel.query.filter_by(
id=bpmn_process.direct_parent_process_id
).first()
assert direct_parent_process is not None
assert direct_parent_process.bpmn_process_definition.bpmn_identifier == "test_process_to_call"
assert processor.get_data() == fifth_data_set assert processor.get_data() == fifth_data_set
def test_does_not_recreate_human_tasks_on_multiple_saves( def test_does_not_recreate_human_tasks_on_multiple_saves(

View File

@ -162,7 +162,7 @@ describe('process-instances', () => {
cy.getBySel('process-instance-log-list-link').click(); cy.getBySel('process-instance-log-list-link').click();
cy.getBySel('process-instance-log-detailed').click(); cy.getBySel('process-instance-log-detailed').click();
cy.contains('process_model_one'); cy.contains('process_model_one');
cy.contains('State change to COMPLETED'); cy.contains('task_completed');
cy.basicPaginationTest(); cy.basicPaginationTest();
}); });
@ -182,6 +182,9 @@ describe('process-instances', () => {
cy.url().should('include', `status=${processStatus}`); cy.url().should('include', `status=${processStatus}`);
cy.assertAtLeastOneItemInPaginatedResults(); cy.assertAtLeastOneItemInPaginatedResults();
cy.getBySel(`process-instance-status-${processStatus}`); cy.getBySel(`process-instance-status-${processStatus}`);
// maybe waiting a bit before trying to click makes this work consistently?
cy.wait(1000);
// there should really only be one, but in CI there are sometimes more // there should really only be one, but in CI there are sometimes more
cy.get('div[aria-label="Clear all selected items"]:first').click(); cy.get('div[aria-label="Clear all selected items"]:first').click();
cy.get('div[aria-label="Clear all selected items"]').should( cy.get('div[aria-label="Clear all selected items"]').should(

View File

@ -154,6 +154,10 @@ Cypress.Commands.add(
.then(($element) => { .then(($element) => {
const oldId = $element.text().trim(); const oldId = $element.text().trim();
cy.get('.cds--pagination__button--forward').click(); cy.get('.cds--pagination__button--forward').click();
cy.contains(
`[data-qa=${dataQaTagToUseToEnsureTableHasLoaded}]`,
oldId
).should('not.exist');
cy.contains(/\b34 of \d+/); cy.contains(/\b34 of \d+/);
cy.get('.cds--pagination__button--backward').click(); cy.get('.cds--pagination__button--backward').click();
cy.contains(/\b12 of \d+/); cy.contains(/\b12 of \d+/);

View File

@ -1295,7 +1295,6 @@ export default function ProcessInstanceListTable({
end_in_seconds: 'End Time', end_in_seconds: 'End Time',
status: 'Status', status: 'Status',
process_initiator_username: 'Started By', process_initiator_username: 'Started By',
spiff_step: 'SpiffWorkflow Step',
}; };
const getHeaderLabel = (header: string) => { const getHeaderLabel = (header: string) => {
return headerLabels[header] ?? header; return headerLabels[header] ?? header;

View File

@ -60,14 +60,14 @@ import HttpService from '../services/HttpService';
import ButtonWithConfirmation from './ButtonWithConfirmation'; import ButtonWithConfirmation from './ButtonWithConfirmation';
import { getBpmnProcessIdentifiers, 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, Task } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService'; import { usePermissionFetcher } from '../hooks/PermissionService';
type OwnProps = { type OwnProps = {
processModelId: string; processModelId: string;
diagramType: string; diagramType: string;
readyOrWaitingProcessInstanceTasks?: ProcessInstanceTask[] | null; readyOrWaitingProcessInstanceTasks?: Task[] | null;
completedProcessInstanceTasks?: ProcessInstanceTask[] | null; completedProcessInstanceTasks?: Task[] | null;
saveDiagram?: (..._args: any[]) => any; saveDiagram?: (..._args: any[]) => any;
onDeleteFile?: (..._args: any[]) => any; onDeleteFile?: (..._args: any[]) => any;
isPrimaryFile?: boolean; isPrimaryFile?: boolean;
@ -364,18 +364,18 @@ export default function ReactDiagramEditor({
function highlightBpmnIoElement( function highlightBpmnIoElement(
canvas: any, canvas: any,
processInstanceTask: ProcessInstanceTask, task: Task,
bpmnIoClassName: string, bpmnIoClassName: string,
bpmnProcessIdentifiers: string[] bpmnProcessIdentifiers: string[]
) { ) {
if (checkTaskCanBeHighlighted(processInstanceTask.name)) { if (checkTaskCanBeHighlighted(task.bpmn_identifier)) {
try { try {
if ( if (
bpmnProcessIdentifiers.includes( bpmnProcessIdentifiers.includes(
processInstanceTask.process_identifier task.bpmn_process_definition_identifier
) )
) { ) {
canvas.addMarker(processInstanceTask.name, bpmnIoClassName); canvas.addMarker(task.bpmn_identifier, bpmnIoClassName);
} }
} catch (bpmnIoError: any) { } catch (bpmnIoError: any) {
// the task list also contains task for processes called from call activities which will // the task list also contains task for processes called from call activities which will

View File

@ -21,8 +21,47 @@ export interface RecentProcessModel {
processModelDisplayName: string; processModelDisplayName: string;
} }
export interface ProcessInstanceTask { export interface TaskPropertiesJson {
parent: string;
}
export interface TaskDefinitionPropertiesJson {
spec: string;
}
export interface EventDefinition {
typename: string;
payload: any;
event_definitions: [EventDefinition];
message_var?: string;
}
export interface Task {
id: number; id: number;
guid: string;
bpmn_identifier: string;
bpmn_name?: string;
bpmn_process_direct_parent_guid: string;
bpmn_process_definition_identifier: string;
data: any;
state: string;
typename: string;
properties_json: TaskPropertiesJson;
task_definition_properties_json: TaskDefinitionPropertiesJson;
event_definition?: EventDefinition;
}
export interface TaskIds {
completed: Task[];
readyOrWaiting: Task[];
}
export interface ProcessInstanceTask {
id: string;
task_id: string; task_id: string;
calling_subprocess_task_id: string; calling_subprocess_task_id: string;
@ -46,7 +85,6 @@ export interface ProcessInstanceTask {
type: string; type: string;
updated_at_in_seconds: number; updated_at_in_seconds: number;
task_spiff_step?: number;
potential_owner_usernames?: string; potential_owner_usernames?: string;
assigned_user_group_identifier?: string; assigned_user_group_identifier?: string;
} }
@ -90,7 +128,6 @@ export interface ProcessInstance {
end_in_seconds: number | null; end_in_seconds: number | null;
process_initiator_username: string; process_initiator_username: string;
bpmn_xml_file_contents?: string; bpmn_xml_file_contents?: string;
spiff_step?: number;
created_at_in_seconds: number; created_at_in_seconds: number;
updated_at_in_seconds: number; updated_at_in_seconds: number;
bpmn_version_control_identifier: string; bpmn_version_control_identifier: string;
@ -258,3 +295,20 @@ export interface JsonSchemaForm {
process_model_id: string; process_model_id: string;
required: string[]; required: string[];
} }
export interface ProcessInstanceLogEntry {
bpmn_process_definition_identifier: string;
bpmn_process_definition_name: string;
bpmn_task_type: string;
event_type: string;
spiff_task_guid: string;
task_definition_identifier: string;
task_guid: string;
timestamp: number;
id: number;
process_instance_id: number;
task_definition_name?: string;
user_id?: number;
username?: string;
}

View File

@ -73,7 +73,7 @@ export default function AdminRoutes() {
element={<ProcessInstanceShow variant="for-me" />} element={<ProcessInstanceShow variant="for-me" />}
/> />
<Route <Route
path="process-instances/for-me/:process_model_id/:process_instance_id/:spiff_step" path="process-instances/for-me/:process_model_id/:process_instance_id/:to_task_guid"
element={<ProcessInstanceShow variant="for-me" />} element={<ProcessInstanceShow variant="for-me" />}
/> />
<Route <Route
@ -81,7 +81,7 @@ export default function AdminRoutes() {
element={<ProcessInstanceShow variant="all" />} 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/:to_task_guid"
element={<ProcessInstanceShow variant="all" />} element={<ProcessInstanceShow variant="all" />}
/> />
<Route <Route

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
// @ts-ignore // @ts-ignore
import { Table, Tabs, TabList, Tab } from '@carbon/react'; import { Table, Tabs, TabList, Tab } from '@carbon/react';
import { useParams, useSearchParams } from 'react-router-dom'; import { Link, useParams, useSearchParams } from 'react-router-dom';
import PaginationForTable from '../components/PaginationForTable'; import PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import { import {
@ -10,6 +10,7 @@ import {
} from '../helpers'; } from '../helpers';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { ProcessInstanceLogEntry } from '../interfaces';
type OwnProps = { type OwnProps = {
variant: string; variant: string;
@ -50,25 +51,25 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
isDetailedView, isDetailedView,
]); ]);
const getTableRow = (row: any) => { const getTableRow = (logEntry: ProcessInstanceLogEntry) => {
const tableRow = []; const tableRow = [];
const taskNameCell = ( const taskNameCell = (
<td> <td>
{row.task_definition_name || {logEntry.task_definition_name ||
(row.bpmn_task_type === 'StartEvent' ? 'Process Started' : '') || (logEntry.bpmn_task_type === 'StartEvent' ? 'Process Started' : '') ||
(row.bpmn_task_type === 'EndEvent' ? 'Process Ended' : '')} (logEntry.bpmn_task_type === 'EndEvent' ? 'Process Ended' : '')}
</td> </td>
); );
const bpmnProcessCell = ( const bpmnProcessCell = (
<td> <td>
{row.bpmn_process_definition_name || {logEntry.bpmn_process_definition_name ||
row.bpmn_process_definition_identifier} logEntry.bpmn_process_definition_identifier}
</td> </td>
); );
if (isDetailedView) { if (isDetailedView) {
tableRow.push( tableRow.push(
<> <>
<td data-qa="paginated-entity-id">{row.id}</td> <td data-qa="paginated-entity-id">{logEntry.id}</td>
{bpmnProcessCell} {bpmnProcessCell}
{taskNameCell} {taskNameCell}
</> </>
@ -84,24 +85,44 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
if (isDetailedView) { if (isDetailedView) {
tableRow.push( tableRow.push(
<> <>
<td>{row.bpmn_task_type}</td> <td>{logEntry.bpmn_task_type}</td>
<td>{row.event_type}</td> <td>{logEntry.event_type}</td>
<td> <td>
{row.username || ( {logEntry.username || (
<span className="system-user-log-entry">system</span> <span className="system-user-log-entry">system</span>
)} )}
</td> </td>
</> </>
); );
} }
tableRow.push(<td>{convertSecondsToFormattedDateTime(row.timestamp)}</td>);
return <tr key={row.id}>{tableRow}</tr>; let timestampComponent = (
<td>{convertSecondsToFormattedDateTime(logEntry.timestamp)}</td>
);
if (logEntry.spiff_task_guid) {
timestampComponent = (
<td>
<Link
data-qa="process-instance-show-link"
to={`${processInstanceShowPageBaseUrl}/${logEntry.process_instance_id}/${logEntry.spiff_task_guid}`}
title="View state when task was completed"
>
{convertSecondsToFormattedDateTime(logEntry.timestamp)}
</Link>
</td>
);
}
tableRow.push(timestampComponent);
return <tr key={logEntry.id}>{tableRow}</tr>;
}; };
const buildTable = () => { const buildTable = () => {
const rows = processInstanceLogs.map((row) => { const rows = processInstanceLogs.map(
return getTableRow(row); (logEntry: ProcessInstanceLogEntry) => {
}); return getTableRow(logEntry);
}
);
const tableHeaders = []; const tableHeaders = [];
if (isDetailedView) { if (isDetailedView) {

View File

@ -7,12 +7,10 @@ import {
useSearchParams, useSearchParams,
} from 'react-router-dom'; } from 'react-router-dom';
import { import {
CaretRight,
TrashCan, TrashCan,
StopOutline, StopOutline,
PauseOutline, PauseOutline,
PlayOutline, PlayOutline,
CaretLeft,
InProgress, InProgress,
Checkmark, Checkmark,
Warning, Warning,
@ -42,11 +40,14 @@ import {
import ButtonWithConfirmation from '../components/ButtonWithConfirmation'; import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { import {
EventDefinition,
PermissionsToCheck, PermissionsToCheck,
ProcessData, ProcessData,
ProcessInstance, ProcessInstance,
ProcessInstanceMetadata, ProcessInstanceMetadata,
ProcessInstanceTask, Task,
TaskDefinitionPropertiesJson,
TaskIds,
} from '../interfaces'; } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService'; import { usePermissionFetcher } from '../hooks/PermissionService';
import ProcessInstanceClass from '../classes/ProcessInstanceClass'; import ProcessInstanceClass from '../classes/ProcessInstanceClass';
@ -64,10 +65,12 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const [processInstance, setProcessInstance] = const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null); useState<ProcessInstance | null>(null);
const [tasks, setTasks] = useState<ProcessInstanceTask[] | null>(null); const [tasks, setTasks] = useState<Task[] | null>(null);
const [tasksCallHadError, setTasksCallHadError] = useState<boolean>(false); const [tasksCallHadError, setTasksCallHadError] = useState<boolean>(false);
const [taskToDisplay, setTaskToDisplay] = const [taskToDisplay, setTaskToDisplay] = useState<Task | null>(null);
useState<ProcessInstanceTask | null>(null); const [taskToTimeTravelTo, setTaskToTimeTravelTo] = useState<Task | null>(
null
);
const [taskDataToDisplay, setTaskDataToDisplay] = useState<string>(''); const [taskDataToDisplay, setTaskDataToDisplay] = useState<string>('');
const [showTaskDataLoading, setShowTaskDataLoading] = const [showTaskDataLoading, setShowTaskDataLoading] =
useState<boolean>(false); useState<boolean>(false);
@ -127,10 +130,23 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
} }
useEffect(() => { useEffect(() => {
if (permissionsLoaded) { if (!permissionsLoaded) {
return undefined;
}
const processTaskFailure = () => { const processTaskFailure = () => {
setTasksCallHadError(true); setTasksCallHadError(true);
}; };
const processTasksSuccess = (results: Task[]) => {
if (params.to_task_guid) {
const matchingTask = results.find(
(task: Task) => task.guid === params.to_task_guid
);
if (matchingTask) {
setTaskToTimeTravelTo(matchingTask);
}
}
setTasks(results);
};
let queryParams = ''; let queryParams = '';
const processIdentifier = searchParams.get('process_identifier'); const processIdentifier = searchParams.get('process_identifier');
if (processIdentifier) { if (processIdentifier) {
@ -144,9 +160,13 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
path: `${apiPath}/${modifiedProcessModelId}/${params.process_instance_id}${queryParams}`, path: `${apiPath}/${modifiedProcessModelId}/${params.process_instance_id}${queryParams}`,
successCallback: setProcessInstance, successCallback: setProcessInstance,
}); });
let taskParams = '?all_tasks=true&most_recent_tasks_only=true'; let taskParams = '?most_recent_tasks_only=true';
if (typeof params.spiff_step !== 'undefined') { if (typeof params.to_task_guid !== 'undefined') {
taskParams = `${taskParams}&spiff_step=${params.spiff_step}`; taskParams = `${taskParams}&to_task_guid=${params.to_task_guid}`;
}
const bpmnProcessGuid = searchParams.get('bpmn_process_guid');
if (bpmnProcessGuid) {
taskParams = `${taskParams}&bpmn_process_guid=${bpmnProcessGuid}`;
} }
let taskPath = ''; let taskPath = '';
if (ability.can('GET', taskListPath)) { if (ability.can('GET', taskListPath)) {
@ -155,13 +175,13 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
if (taskPath) { if (taskPath) {
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: taskPath, path: taskPath,
successCallback: setTasks, successCallback: processTasksSuccess,
failureCallback: processTaskFailure, failureCallback: processTaskFailure,
}); });
} else { } else {
setTasksCallHadError(true); setTasksCallHadError(true);
} }
} return undefined;
}, [ }, [
targetUris, targetUris,
params, params,
@ -211,21 +231,14 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
}; };
const getTaskIds = () => { const getTaskIds = () => {
const taskIds = { completed: [], readyOrWaiting: [] }; const taskIds: TaskIds = { completed: [], readyOrWaiting: [] };
if (tasks) { if (tasks) {
const callingSubprocessId = searchParams.get('call_activity_task_id'); tasks.forEach(function getUserTasksElement(task: Task) {
tasks.forEach(function getUserTasksElement(task: ProcessInstanceTask) {
if (
callingSubprocessId &&
callingSubprocessId !== task.calling_subprocess_task_id
) {
return null;
}
if (task.state === 'COMPLETED') { if (task.state === 'COMPLETED') {
(taskIds.completed as any).push(task); taskIds.completed.push(task);
} }
if (task.state === 'READY' || task.state === 'WAITING') { if (task.state === 'READY' || task.state === 'WAITING') {
(taskIds.readyOrWaiting as any).push(task); taskIds.readyOrWaiting.push(task);
} }
return null; return null;
}); });
@ -233,31 +246,29 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return taskIds; return taskIds;
}; };
const currentSpiffStep = () => { const currentToTaskGuid = () => {
if (processInstance && typeof params.spiff_step === 'undefined') { if (taskToTimeTravelTo) {
return processInstance.spiff_step || 0; return taskToTimeTravelTo.guid;
} }
return null;
return Number(params.spiff_step);
}; };
const showingFirstSpiffStep = () => { // right now this just assume if taskToTimeTravelTo was passed in then
return currentSpiffStep() === 1; // this cannot be the active task.
// we may need a better way to figure this out.
const showingActiveTask = () => {
return !taskToTimeTravelTo;
}; };
const showingLastSpiffStep = () => { const completionViewLink = (label: any, taskGuid: string) => {
return processInstance && currentSpiffStep() === processInstance.spiff_step;
};
const spiffStepLink = (label: any, spiffStep: number) => {
const processIdentifier = searchParams.get('process_identifier'); const processIdentifier = searchParams.get('process_identifier');
const callActivityTaskId = searchParams.get('call_activity_task_id'); const callActivityTaskId = searchParams.get('bpmn_process_guid');
const queryParamArray = []; const queryParamArray = [];
if (processIdentifier) { if (processIdentifier) {
queryParamArray.push(`process_identifier=${processIdentifier}`); queryParamArray.push(`process_identifier=${processIdentifier}`);
} }
if (callActivityTaskId) { if (callActivityTaskId) {
queryParamArray.push(`call_activity_task_id=${callActivityTaskId}`); queryParamArray.push(`bpmn_process_guid=${callActivityTaskId}`);
} }
let queryParams = ''; let queryParams = '';
if (queryParamArray.length > 0) { if (queryParamArray.length > 0) {
@ -268,37 +279,21 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
<Link <Link
reloadDocument reloadDocument
data-qa="process-instance-step-link" data-qa="process-instance-step-link"
to={`${processInstanceShowPageBaseUrl}/${spiffStep}${queryParams}`} to={`${processInstanceShowPageBaseUrl}/${taskGuid}${queryParams}`}
> >
{label} {label}
</Link> </Link>
); );
}; };
const previousStepLink = () => { const returnToProcessInstance = () => {
if (showingFirstSpiffStep()) {
return null;
}
return spiffStepLink(<CaretLeft />, currentSpiffStep() - 1);
};
const nextStepLink = () => {
if (showingLastSpiffStep()) {
return null;
}
return spiffStepLink(<CaretRight />, currentSpiffStep() + 1);
};
const returnToLastSpiffStep = () => {
window.location.href = processInstanceShowPageBaseUrl; window.location.href = processInstanceShowPageBaseUrl;
}; };
const resetProcessInstance = () => { const resetProcessInstance = () => {
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `${targetUris.processInstanceResetPath}/${currentSpiffStep()}`, path: `${targetUris.processInstanceResetPath}/${currentToTaskGuid()}`,
successCallback: returnToLastSpiffStep, successCallback: returnToProcessInstance,
httpMethod: 'POST', httpMethod: 'POST',
}); });
}; };
@ -509,7 +504,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return <div />; return <div />;
}; };
const processTaskResult = (result: ProcessInstanceTask) => { const processTaskResult = (result: Task) => {
if (result == null) { if (result == null) {
setTaskDataToDisplay(''); setTaskDataToDisplay('');
} else { } else {
@ -518,15 +513,15 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
setShowTaskDataLoading(false); setShowTaskDataLoading(false);
}; };
const initializeTaskDataToDisplay = (task: ProcessInstanceTask | null) => { const initializeTaskDataToDisplay = (task: Task | null) => {
if ( if (
task && task &&
task.state === 'COMPLETED' && (task.state === 'COMPLETED' || task.state === 'READY') &&
ability.can('GET', targetUris.processInstanceTaskDataPath) ability.can('GET', targetUris.processInstanceTaskDataPath)
) { ) {
setShowTaskDataLoading(true); setShowTaskDataLoading(true);
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `${targetUris.processInstanceTaskDataPath}/${task.task_spiff_step}`, path: `${targetUris.processInstanceTaskDataPath}/${task.guid}`,
httpMethod: 'GET', httpMethod: 'GET',
successCallback: processTaskResult, successCallback: processTaskResult,
failureCallback: (error: any) => { failureCallback: (error: any) => {
@ -577,13 +572,12 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
successCallback: handleProcessDataShowResponse, successCallback: handleProcessDataShowResponse,
}); });
} else if (tasks) { } else if (tasks) {
const matchingTask: any = tasks.find((task: any) => { const matchingTask: Task | undefined = tasks.find((task: Task) => {
const callingSubprocessId = searchParams.get('call_activity_task_id');
return ( return (
(!callingSubprocessId || task.bpmn_identifier === shapeElement.id &&
callingSubprocessId === task.calling_subprocess_task_id) && bpmnProcessIdentifiers.includes(
task.name === shapeElement.id && task.bpmn_process_definition_identifier
bpmnProcessIdentifiers.includes(task.process_identifier) )
); );
}); });
if (matchingTask) { if (matchingTask) {
@ -600,7 +594,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const getTaskById = (taskId: string) => { const getTaskById = (taskId: string) => {
if (tasks !== null) { if (tasks !== null) {
return tasks.find((task: any) => task.id === taskId); return tasks.find((task: Task) => task.guid === taskId) || null;
} }
return null; return null;
}; };
@ -609,81 +603,88 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
console.log('result', result); console.log('result', result);
}; };
const getParentTaskFromTask = (task: Task) => {
return task.properties_json.parent;
};
const createScriptUnitTest = () => { const createScriptUnitTest = () => {
if (taskToDisplay) { if (taskToDisplay) {
const taskToUse: any = taskToDisplay; const previousTask: Task | null = getTaskById(
const previousTask: any = getTaskById(taskToUse.parent); getParentTaskFromTask(taskToDisplay)
);
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/script-unit-tests`, path: `/process-models/${modifiedProcessModelId}/script-unit-tests`,
httpMethod: 'POST', httpMethod: 'POST',
successCallback: processScriptUnitTestCreateResult, successCallback: processScriptUnitTestCreateResult,
postBody: { postBody: {
bpmn_task_identifier: taskToUse.name, bpmn_task_identifier: taskToDisplay.bpmn_identifier,
input_json: previousTask.data, input_json: previousTask ? previousTask.data : '',
expected_output_json: taskToUse.data, expected_output_json: taskToDisplay.data,
}, },
}); });
} }
}; };
const isCurrentTask = (task: any) => { const isActiveTask = (task: Task) => {
const subprocessTypes = [ const subprocessTypes = [
'Subprocess', 'Subprocess',
'Call Activity', 'CallActivity',
'Transactional Subprocess', 'Transactional Subprocess',
]; ];
return ( return (
(task.state === 'WAITING' && (task.state === 'WAITING' &&
subprocessTypes.filter((t) => t === task.type).length > 0) || subprocessTypes.filter((t) => t === task.typename).length > 0) ||
task.state === 'READY' task.state === 'READY'
); );
}; };
const canEditTaskData = (task: any) => { const canEditTaskData = (task: Task) => {
return ( return (
processInstance && processInstance &&
ability.can('PUT', targetUris.processInstanceTaskDataPath) && ability.can('PUT', targetUris.processInstanceTaskDataPath) &&
isCurrentTask(task) && isActiveTask(task) &&
processInstance.status === 'suspended' && processInstance.status === 'suspended' &&
showingLastSpiffStep() showingActiveTask()
); );
}; };
const canSendEvent = (task: any) => { const canSendEvent = (task: Task) => {
// We actually could allow this for any waiting events // We actually could allow this for any waiting events
const taskTypes = ['Event Based Gateway']; const taskTypes = ['Event Based Gateway'];
return ( return (
processInstance && processInstance &&
processInstance.status === 'waiting' && processInstance.status === 'waiting' &&
ability.can('POST', targetUris.processInstanceSendEventPath) && ability.can('POST', targetUris.processInstanceSendEventPath) &&
taskTypes.filter((t) => t === task.type).length > 0 && taskTypes.filter((t) => t === task.typename).length > 0 &&
task.state === 'WAITING' && task.state === 'WAITING' &&
showingLastSpiffStep() showingActiveTask()
); );
}; };
const canCompleteTask = (task: any) => { const canCompleteTask = (task: Task) => {
return ( return (
processInstance && processInstance &&
processInstance.status === 'suspended' && processInstance.status === 'suspended' &&
ability.can('POST', targetUris.processInstanceCompleteTaskPath) && ability.can('POST', targetUris.processInstanceCompleteTaskPath) &&
isCurrentTask(task) && isActiveTask(task) &&
showingLastSpiffStep() showingActiveTask()
); );
}; };
const canResetProcess = (task: any) => { const canResetProcess = (_task: Task) => {
return ( // disabling this feature for now
ability.can('POST', targetUris.processInstanceResetPath) && return false;
processInstance && // return (
processInstance.status === 'suspended' && // ability.can('POST', targetUris.processInstanceResetPath) &&
task.state === 'READY' && // processInstance &&
!showingLastSpiffStep() // processInstance.status === 'suspended' &&
); // task.state === 'READY' &&
// !showingActiveTask()
// );
}; };
const getEvents = (task: any) => { const getEvents = (task: Task) => {
const handleMessage = (eventDefinition: any) => { const handleMessage = (eventDefinition: EventDefinition) => {
if (eventDefinition.typename === 'MessageEventDefinition') { if (eventDefinition.typename === 'MessageEventDefinition') {
const newEvent = eventDefinition; const newEvent = eventDefinition;
delete newEvent.message_var; delete newEvent.message_var;
@ -693,7 +694,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return eventDefinition; return eventDefinition;
}; };
if (task.event_definition && task.event_definition.event_definitions) if (task.event_definition && task.event_definition.event_definitions)
return task.event_definition.event_definitions.map((e: any) => return task.event_definition.event_definitions.map((e: EventDefinition) =>
handleMessage(e) handleMessage(e)
); );
if (task.event_definition) return [handleMessage(task.event_definition)]; if (task.event_definition) return [handleMessage(task.event_definition)];
@ -717,7 +718,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
setEditingTaskData(false); setEditingTaskData(false);
const dataObject = taskDataStringToObject(taskDataToDisplay); const dataObject = taskDataStringToObject(taskDataToDisplay);
if (taskToDisplay) { if (taskToDisplay) {
const taskToDisplayCopy: ProcessInstanceTask = { const taskToDisplayCopy: Task = {
...taskToDisplay, ...taskToDisplay,
data: dataObject, data: dataObject,
}; // spread operator }; // spread operator
@ -730,13 +731,12 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
if (!taskToDisplay) { if (!taskToDisplay) {
return; return;
} }
console.log('saveTaskData');
removeError(); removeError();
// taskToUse is copy of taskToDisplay, with taskDataToDisplay in data attribute // taskToUse is copy of taskToDisplay, with taskDataToDisplay in data attribute
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay }; const taskToUse: Task = { ...taskToDisplay, data: taskDataToDisplay };
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `${targetUris.processInstanceTaskDataPath}/${taskToUse.id}`, path: `${targetUris.processInstanceTaskDataPath}/${taskToUse.guid}`,
httpMethod: 'PUT', httpMethod: 'PUT',
successCallback: saveTaskDataResult, successCallback: saveTaskDataResult,
failureCallback: addError, failureCallback: addError,
@ -759,20 +759,21 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
}; };
const completeTask = (execute: boolean) => { const completeTask = (execute: boolean) => {
const taskToUse: any = taskToDisplay; if (taskToDisplay) {
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/task-complete/${modifiedProcessModelId}/${params.process_instance_id}/${taskToUse.id}`, path: `/task-complete/${modifiedProcessModelId}/${params.process_instance_id}/${taskToDisplay.guid}`,
httpMethod: 'POST', httpMethod: 'POST',
successCallback: returnToLastSpiffStep, successCallback: returnToProcessInstance,
postBody: { execute }, postBody: { execute },
}); });
}
}; };
const taskDisplayButtons = (task: any) => { const taskDisplayButtons = (task: Task) => {
const buttons = []; const buttons = [];
if ( if (
task.type === 'Script Task' && task.typename === 'Script Task' &&
ability.can('PUT', targetUris.processModelShowPath) ability.can('PUT', targetUris.processModelShowPath)
) { ) {
buttons.push( buttons.push(
@ -785,11 +786,15 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
); );
} }
if (task.type === 'Call Activity') { if (task.typename === 'CallActivity') {
console.log('task', task);
const taskDefinitionPropertiesJson: TaskDefinitionPropertiesJson =
task.task_definition_properties_json;
console.log('taskDefinitionPropertiesJson', taskDefinitionPropertiesJson);
buttons.push( buttons.push(
<Link <Link
data-qa="go-to-call-activity-result" data-qa="go-to-call-activity-result"
to={`${window.location.pathname}?process_identifier=${task.call_activity_process_identifier}&call_activity_task_id=${task.id}`} to={`${window.location.pathname}?process_identifier=${taskDefinitionPropertiesJson.spec}&bpmn_process_guid=${task.guid}`}
target="_blank" target="_blank"
> >
View Call Activity Diagram View Call Activity Diagram
@ -971,12 +976,15 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
}; };
const taskUpdateDisplayArea = () => { const taskUpdateDisplayArea = () => {
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay }; if (!taskToDisplay) {
return null;
}
const taskToUse: Task = { ...taskToDisplay, data: taskDataToDisplay };
const candidateEvents: any = getEvents(taskToUse); const candidateEvents: any = getEvents(taskToUse);
if (taskToDisplay) { if (taskToDisplay) {
let taskTitleText = taskToUse.id; let taskTitleText = taskToUse.guid;
if (taskToUse.title) { if (taskToUse.bpmn_name) {
taskTitleText += ` (${taskToUse.title})`; taskTitleText += ` (${taskToUse.bpmn_name})`;
} }
return ( return (
<Modal <Modal
@ -985,18 +993,17 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
onRequestClose={handleTaskDataDisplayClose} onRequestClose={handleTaskDataDisplayClose}
> >
<Stack orientation="horizontal" gap={2}> <Stack orientation="horizontal" gap={2}>
<span title={taskTitleText}>{taskToUse.name}</span> ( <span title={taskTitleText}>{taskToUse.bpmn_identifier}</span> (
{taskToUse.type} {taskToUse.typename}
): {taskToUse.state} ): {taskToUse.state}
{taskDisplayButtons(taskToUse)} {taskDisplayButtons(taskToUse)}
</Stack> </Stack>
{taskToUse.task_spiff_step ? ( {taskToUse.state === 'COMPLETED' ? (
<div> <div>
<Stack orientation="horizontal" gap={2}> <Stack orientation="horizontal" gap={2}>
Task completed at step:{' '} {completionViewLink(
{spiffStepLink( 'View process instance at the time when this task was active.',
`${taskToUse.task_spiff_step}`, taskToUse.guid
taskToUse.task_spiff_step
)} )}
</Stack> </Stack>
<br /> <br />
@ -1012,23 +1019,6 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return null; return null;
}; };
const stepsElement = () => {
if (!processInstance) {
return null;
}
return (
<Grid condensed fullWidth>
<Column sm={3} md={3} lg={3}>
<Stack orientation="horizontal" gap={3} className="smaller-text">
{previousStepLink()}
Step {currentSpiffStep()} of {processInstance.spiff_step}
{nextStepLink()}
</Stack>
</Column>
</Grid>
);
};
const buttonIcons = () => { const buttonIcons = () => {
if (!processInstance) { if (!processInstance) {
return null; return null;
@ -1063,6 +1053,39 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return elements; return elements;
}; };
const viewMostRecentStateComponent = () => {
if (!taskToTimeTravelTo) {
return null;
}
const title = `${taskToTimeTravelTo.id}: ${taskToTimeTravelTo.guid}: ${taskToTimeTravelTo.bpmn_identifier}`;
return (
<>
<Grid condensed fullWidth>
<Column md={8} lg={16} sm={4}>
<p>
Viewing process instance at the time when{' '}
<span title={title}>
<strong>
{taskToTimeTravelTo.bpmn_name ||
taskToTimeTravelTo.bpmn_identifier}
</strong>
</span>{' '}
was active.{' '}
<Link
reloadDocument
data-qa="process-instance-view-active-task-link"
to={processInstanceShowPageBaseUrl}
>
View current process instance state.
</Link>
</p>
</Column>
</Grid>
<br />
</>
);
};
if (processInstance && (tasks || tasksCallHadError)) { if (processInstance && (tasks || tasksCallHadError)) {
const taskIds = getTaskIds(); const taskIds = getTaskIds();
const processModelId = unModifyProcessIdentifierForPathParam( const processModelId = unModifyProcessIdentifierForPathParam(
@ -1116,8 +1139,8 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
{taskUpdateDisplayArea()} {taskUpdateDisplayArea()}
{processDataDisplayArea()} {processDataDisplayArea()}
{processInstanceMetadataArea()} {processInstanceMetadataArea()}
{stepsElement()}
<br /> <br />
{viewMostRecentStateComponent()}
<ReactDiagramEditor <ReactDiagramEditor
processModelId={processModelId || ''} processModelId={processModelId || ''}
diagramXML={processInstance.bpmn_xml_file_contents || ''} diagramXML={processInstance.bpmn_xml_file_contents || ''}