Feature/business end states (#333)
* WIP: some updates to support new spiff w/ burnettk * unit tests are passing * all tests except message tests are passing * fixed usage of catch message event w/ burnettk * messages are working again w/ burnettk * uncommented remaining message tests w/ burnettk * fixed cypress tests w/ burnettk * use main for spiffworkflow * translated mysql last milestone query to sqlalchemy w/ burnettk * fixed last milestone query so instances still return if no milestone found and moved some code from the main report method to own methods * added some comments * added last milestone column to process instances table * display last milestone in instance list table w/ burnettk * remove 3 characters when truncating last milestone for ellipsis * make sure we have a current processor so we don't return null * remove sleep * The background processor now only picks up processes that were last updated more than a minute ago to avoid conflicting with the interstitial page. With the understanding that we can rmeove this limitation when we can refactor to allow the backend processes to provide updates on what they are doing. * pyl w/ burnettk * cache last milestone on instances * pyl * added test for last milestone and added it to the proces instance show page w/ burnettk * fixed broken test w/ burnettk * fixed last milestone header * removed duplicated column * fixed broken test --------- Co-authored-by: jasquat <jasquat@users.noreply.github.com> Co-authored-by: Kevin Burnett <18027+burnettk@users.noreply.github.com> Co-authored-by: danfunk <daniel.h.funk@gmail.com> Co-authored-by: burnettk <burnettk@users.noreply.github.com>
This commit is contained in:
parent
f218805a2d
commit
9bb9ce47f8
|
@ -34,6 +34,7 @@ on:
|
|||
- feature/event-payloads-part-2
|
||||
- feature/event-payload-migration-fix
|
||||
- spiffdemo
|
||||
- feature/business_end_states
|
||||
- feature/allow-markdown-in-extension-results
|
||||
|
||||
jobs:
|
||||
|
|
|
@ -39,6 +39,7 @@ function run_autofixers() {
|
|||
fi
|
||||
|
||||
python_dirs="$(get_python_dirs) bin"
|
||||
# shellcheck disable=2086
|
||||
ruff --fix $python_dirs || echo ''
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ trap 'error_handler ${LINENO} $?' ERR
|
|||
set -o errtrace -o errexit -o nounset -o pipefail
|
||||
|
||||
mysql -uroot spiffworkflow_backend_local_development -e '
|
||||
SELECT td.bpmn_identifier FROM process_instance p
|
||||
SELECT td.bpmn_identifier, p.id FROM process_instance p
|
||||
|
||||
JOIN process_instance_event pie ON pie.process_instance_id = p.id
|
||||
JOIN task t ON t.guid = pie.task_guid
|
||||
|
@ -27,6 +27,6 @@ mysql -uroot spiffworkflow_backend_local_development -e '
|
|||
GROUP BY pie.process_instance_id
|
||||
) AS max_pie ON max_pie.max_pie_id = pie.id
|
||||
|
||||
WHERE pie.process_instance_id = 27
|
||||
# WHERE pie.process_instance_id = 27
|
||||
;
|
||||
'
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: f04cbd9f43ec
|
||||
Revises: 1073364bc015
|
||||
Create Date: 2023-08-23 11:05:04.368165
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f04cbd9f43ec'
|
||||
down_revision = '1073364bc015'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('process_instance', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('last_milestone_bpmn_name', sa.String(length=255), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('process_instance', schema=None) as batch_op:
|
||||
batch_op.drop_column('last_milestone_bpmn_name')
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -95,6 +95,7 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
|
|||
|
||||
bpmn_version_control_type: str = db.Column(db.String(50))
|
||||
bpmn_version_control_identifier: str = db.Column(db.String(255))
|
||||
last_milestone_bpmn_name: str = db.Column(db.String(255))
|
||||
|
||||
bpmn_xml_file_contents: str | None = None
|
||||
process_model_with_diagram_identifier: str | None = None
|
||||
|
@ -106,19 +107,20 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
|
|||
"""Return object data in serializeable format."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"process_model_identifier": self.process_model_identifier,
|
||||
"process_model_display_name": self.process_model_display_name,
|
||||
"status": self.status,
|
||||
"start_in_seconds": self.start_in_seconds,
|
||||
"end_in_seconds": self.end_in_seconds,
|
||||
"created_at_in_seconds": self.created_at_in_seconds,
|
||||
"updated_at_in_seconds": self.updated_at_in_seconds,
|
||||
"process_initiator_id": self.process_initiator_id,
|
||||
"bpmn_xml_file_contents": self.bpmn_xml_file_contents,
|
||||
"bpmn_version_control_identifier": self.bpmn_version_control_identifier,
|
||||
"bpmn_version_control_type": self.bpmn_version_control_type,
|
||||
"bpmn_xml_file_contents": self.bpmn_xml_file_contents,
|
||||
"created_at_in_seconds": self.created_at_in_seconds,
|
||||
"end_in_seconds": self.end_in_seconds,
|
||||
"last_milestone_bpmn_name": self.last_milestone_bpmn_name,
|
||||
"process_initiator_id": self.process_initiator_id,
|
||||
"process_initiator_username": self.process_initiator.username,
|
||||
"process_model_display_name": self.process_model_display_name,
|
||||
"process_model_identifier": self.process_model_identifier,
|
||||
"start_in_seconds": self.start_in_seconds,
|
||||
"status": self.status,
|
||||
"task_updated_at_in_seconds": self.task_updated_at_in_seconds,
|
||||
"updated_at_in_seconds": self.updated_at_in_seconds,
|
||||
}
|
||||
|
||||
def serialized_with_metadata(self) -> dict[str, Any]:
|
||||
|
|
|
@ -203,7 +203,9 @@ def task_data_update(
|
|||
if json_data_dict is not None:
|
||||
JsonDataModel.insert_or_update_json_data_records({json_data_dict["hash"]: json_data_dict})
|
||||
ProcessInstanceTmpService.add_event_to_process_instance(
|
||||
process_instance, ProcessInstanceEventType.task_data_edited.value, task_guid=task_guid
|
||||
process_instance,
|
||||
ProcessInstanceEventType.task_data_edited.value,
|
||||
task_guid=task_guid,
|
||||
)
|
||||
try:
|
||||
db.session.commit()
|
||||
|
|
|
@ -5,6 +5,7 @@ from typing import Any
|
|||
|
||||
import sqlalchemy
|
||||
from flask import current_app
|
||||
from flask_sqlalchemy.query import Query
|
||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
|
||||
from spiffworkflow_backend.models.group import GroupModel
|
||||
|
@ -16,6 +17,7 @@ from spiffworkflow_backend.models.process_instance_report import FilterValue
|
|||
from spiffworkflow_backend.models.process_instance_report import ProcessInstanceReportModel
|
||||
from spiffworkflow_backend.models.process_instance_report import ReportMetadata
|
||||
from spiffworkflow_backend.models.process_instance_report import ReportMetadataColumn
|
||||
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
|
||||
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
||||
|
@ -61,6 +63,7 @@ class ProcessInstanceReportService:
|
|||
},
|
||||
{"Header": "Start time", "accessor": "start_in_seconds", "filterable": False},
|
||||
{"Header": "End time", "accessor": "end_in_seconds", "filterable": False},
|
||||
{"Header": "Last milestone", "accessor": "last_milestone_bpmn_name", "filterable": False},
|
||||
{"Header": "Status", "accessor": "status", "filterable": False},
|
||||
],
|
||||
"filter_by": [
|
||||
|
@ -96,6 +99,7 @@ class ProcessInstanceReportService:
|
|||
{"Header": "Waiting for", "accessor": "waiting_for", "filterable": False},
|
||||
{"Header": "Started", "accessor": "start_in_seconds", "filterable": False},
|
||||
{"Header": "Last updated", "accessor": "task_updated_at_in_seconds", "filterable": False},
|
||||
{"Header": "Last milestone", "accessor": "last_milestone_bpmn_name", "filterable": False},
|
||||
{"Header": "Status", "accessor": "status", "filterable": False},
|
||||
],
|
||||
"filter_by": [
|
||||
|
@ -121,6 +125,7 @@ class ProcessInstanceReportService:
|
|||
{"Header": "Started by", "accessor": "process_initiator_username", "filterable": False},
|
||||
{"Header": "Started", "accessor": "start_in_seconds", "filterable": False},
|
||||
{"Header": "Last updated", "accessor": "task_updated_at_in_seconds", "filterable": False},
|
||||
{"Header": "Last milestone", "accessor": "last_milestone_bpmn_name", "filterable": False},
|
||||
],
|
||||
"filter_by": [
|
||||
{"field_name": "instances_with_tasks_waiting_for_me", "field_value": True, "operator": "equals"},
|
||||
|
@ -145,6 +150,7 @@ class ProcessInstanceReportService:
|
|||
{"Header": "Started by", "accessor": "process_initiator_username", "filterable": False},
|
||||
{"Header": "Started", "accessor": "start_in_seconds", "filterable": False},
|
||||
{"Header": "Last updated", "accessor": "task_updated_at_in_seconds", "filterable": False},
|
||||
{"Header": "Last milestone", "accessor": "last_milestone_bpmn_name", "filterable": False},
|
||||
],
|
||||
"filter_by": [
|
||||
{"field_name": "process_status", "field_value": active_status_values, "operator": "equals"},
|
||||
|
@ -244,6 +250,11 @@ class ProcessInstanceReportService:
|
|||
metadata_column["accessor"]
|
||||
]
|
||||
|
||||
if "last_milestone_bpmn_name" in process_instance_mapping:
|
||||
process_instance_dict["last_milestone_bpmn_name"] = process_instance_mapping[
|
||||
"last_milestone_bpmn_name"
|
||||
]
|
||||
|
||||
results.append(process_instance_dict)
|
||||
return results
|
||||
|
||||
|
@ -315,7 +326,7 @@ class ProcessInstanceReportService:
|
|||
|
||||
@classmethod
|
||||
def non_metadata_columns(cls) -> list[str]:
|
||||
return cls.process_instance_stock_columns() + ["process_initiator_username"]
|
||||
return cls.process_instance_stock_columns() + ["process_initiator_username", "last_milestone_bpmn_name"]
|
||||
|
||||
@classmethod
|
||||
def builtin_column_options(cls) -> list[ReportMetadataColumn]:
|
||||
|
@ -334,6 +345,7 @@ class ProcessInstanceReportService:
|
|||
"accessor": "process_initiator_username",
|
||||
"filterable": False,
|
||||
},
|
||||
{"Header": "Last milestone", "accessor": "last_milestone_bpmn_name", "filterable": False},
|
||||
{"Header": "Status", "accessor": "status", "filterable": False},
|
||||
]
|
||||
return return_value
|
||||
|
@ -369,6 +381,49 @@ class ProcessInstanceReportService:
|
|||
if filter_found is False:
|
||||
filters.append(new_filter)
|
||||
|
||||
@classmethod
|
||||
def filter_by_user_group_identifier(
|
||||
cls,
|
||||
process_instance_query: Query,
|
||||
user_group_identifier: str,
|
||||
user: UserModel,
|
||||
human_task_already_joined: bool | None = False,
|
||||
process_status: str | None = None,
|
||||
instances_with_tasks_waiting_for_me: bool | None = False,
|
||||
) -> Query:
|
||||
group_model_join_conditions = [GroupModel.id == HumanTaskModel.lane_assignment_id]
|
||||
if user_group_identifier:
|
||||
group_model_join_conditions.append(GroupModel.identifier == user_group_identifier)
|
||||
|
||||
if human_task_already_joined is False:
|
||||
process_instance_query = process_instance_query.join(HumanTaskModel) # type: ignore
|
||||
if process_status is not None:
|
||||
non_active_statuses = [
|
||||
s for s in process_status.split(",") if s not in ProcessInstanceModel.active_statuses()
|
||||
]
|
||||
if len(non_active_statuses) == 0:
|
||||
process_instance_query = process_instance_query.filter(
|
||||
HumanTaskModel.completed.is_(False) # type: ignore
|
||||
)
|
||||
# Check to make sure the task is not only available for the group but the user as well
|
||||
if instances_with_tasks_waiting_for_me is not True:
|
||||
human_task_user_alias = aliased(HumanTaskUserModel)
|
||||
process_instance_query = process_instance_query.join( # type: ignore
|
||||
human_task_user_alias,
|
||||
and_(
|
||||
human_task_user_alias.human_task_id == HumanTaskModel.id,
|
||||
human_task_user_alias.user_id == user.id,
|
||||
),
|
||||
)
|
||||
|
||||
process_instance_query = process_instance_query.join(GroupModel, and_(*group_model_join_conditions)) # type: ignore
|
||||
process_instance_query = process_instance_query.join( # type: ignore
|
||||
UserGroupAssignmentModel,
|
||||
UserGroupAssignmentModel.group_id == GroupModel.id,
|
||||
)
|
||||
process_instance_query = process_instance_query.filter(UserGroupAssignmentModel.user_id == user.id)
|
||||
return process_instance_query
|
||||
|
||||
@classmethod
|
||||
def run_process_instance_report(
|
||||
cls,
|
||||
|
@ -513,38 +568,14 @@ class ProcessInstanceReportService:
|
|||
raise ProcessInstanceReportCannotBeRunError(
|
||||
"A user must be specified to run report with a group identifier."
|
||||
)
|
||||
group_model_join_conditions = [GroupModel.id == HumanTaskModel.lane_assignment_id]
|
||||
if user_group_identifier:
|
||||
group_model_join_conditions.append(GroupModel.identifier == user_group_identifier)
|
||||
|
||||
if human_task_already_joined is False:
|
||||
process_instance_query = process_instance_query.join(HumanTaskModel)
|
||||
if process_status is not None:
|
||||
non_active_statuses = [
|
||||
s for s in process_status.split(",") if s not in ProcessInstanceModel.active_statuses()
|
||||
]
|
||||
if len(non_active_statuses) == 0:
|
||||
process_instance_query = process_instance_query.filter(
|
||||
HumanTaskModel.completed.is_(False) # type: ignore
|
||||
)
|
||||
|
||||
# Check to make sure the task is not only available for the group but the user as well
|
||||
if instances_with_tasks_waiting_for_me is not True:
|
||||
human_task_user_alias = aliased(HumanTaskUserModel)
|
||||
process_instance_query = process_instance_query.join(
|
||||
human_task_user_alias,
|
||||
and_(
|
||||
human_task_user_alias.human_task_id == HumanTaskModel.id,
|
||||
human_task_user_alias.user_id == user.id,
|
||||
),
|
||||
)
|
||||
|
||||
process_instance_query = process_instance_query.join(GroupModel, and_(*group_model_join_conditions))
|
||||
process_instance_query = process_instance_query.join(
|
||||
UserGroupAssignmentModel,
|
||||
UserGroupAssignmentModel.group_id == GroupModel.id,
|
||||
process_instance_query = cls.filter_by_user_group_identifier(
|
||||
process_instance_query=process_instance_query,
|
||||
user_group_identifier=user_group_identifier,
|
||||
user=user,
|
||||
human_task_already_joined=human_task_already_joined,
|
||||
process_status=process_status,
|
||||
instances_with_tasks_waiting_for_me=instances_with_tasks_waiting_for_me,
|
||||
)
|
||||
process_instance_query = process_instance_query.filter(UserGroupAssignmentModel.user_id == user.id)
|
||||
|
||||
instance_metadata_aliases = {}
|
||||
if report_metadata["columns"] is None or len(report_metadata["columns"]) < 1:
|
||||
|
|
|
@ -221,7 +221,6 @@ class ProcessInstanceService:
|
|||
def do_waiting(cls, status_value: str) -> None:
|
||||
run_at_in_seconds_threshold = round(time.time())
|
||||
min_age_in_seconds = 60 # to avoid conflicts with the interstitial page, we wait 60 seconds before processing
|
||||
# min_age_in_seconds = 0 # to avoid conflicts with the interstitial page, we wait 60 seconds before processing
|
||||
process_instance_ids_to_check = ProcessInstanceQueueService.peek_many(
|
||||
status_value, run_at_in_seconds_threshold, min_age_in_seconds
|
||||
)
|
||||
|
|
|
@ -279,7 +279,11 @@ class ProcessModelService(FileSystemService):
|
|||
full_group_id_path = os.path.join(full_group_id_path, process_group_id_segment) # type: ignore
|
||||
parent_group = process_group_cache.get(full_group_id_path, None)
|
||||
if parent_group is None:
|
||||
parent_group = ProcessModelService.get_process_group(full_group_id_path)
|
||||
try:
|
||||
parent_group = ProcessModelService.get_process_group(full_group_id_path)
|
||||
except ProcessEntityNotFoundError:
|
||||
# if parent_group can no longer be found then do not add it to the cache
|
||||
parent_group = None
|
||||
|
||||
if parent_group:
|
||||
if full_group_id_path not in process_group_cache:
|
||||
|
|
|
@ -160,6 +160,17 @@ class TaskModelSavingDelegate(EngineStepDelegate):
|
|||
# # self._add_parents(spiff_task)
|
||||
|
||||
self.last_completed_spiff_task = spiff_task
|
||||
if (
|
||||
spiff_task.task_spec.__class__.__name__ in ["StartEvent", "EndEvent", "IntermediateThrowEvent"]
|
||||
and spiff_task.task_spec.bpmn_name is not None
|
||||
):
|
||||
self.process_instance.last_milestone_bpmn_name = spiff_task.task_spec.bpmn_name
|
||||
elif spiff_task.workflow.parent_task_id is None:
|
||||
# if parent_task_id is None then this should be the top level process
|
||||
if spiff_task.task_spec.__class__.__name__ == "EndEvent":
|
||||
self.process_instance.last_milestone_bpmn_name = "Completed"
|
||||
elif spiff_task.task_spec.__class__.__name__ == "StartEvent":
|
||||
self.process_instance.last_milestone_bpmn_name = "Started"
|
||||
self.process_instance.task_updated_at_in_seconds = round(time.time())
|
||||
if self.secondary_engine_step_delegate:
|
||||
self.secondary_engine_step_delegate.did_complete_task(spiff_task)
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
<?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:process id="Process_caw9m9m" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_088o54g</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_088o54g" sourceRef="StartEvent_1" targetRef="milestone_started" />
|
||||
<bpmn:endEvent id="Event_12t9aet">
|
||||
<bpmn:incoming>Flow_062tiay</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_06ih0jm" sourceRef="milestone_started" targetRef="call_activity" />
|
||||
<bpmn:manualTask id="milestone_started">
|
||||
<bpmn:incoming>Flow_088o54g</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_06ih0jm</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:sequenceFlow id="Flow_0c0z9vd" sourceRef="call_activity" targetRef="milestone_done_with_call_activity" />
|
||||
<bpmn:callActivity id="call_activity" calledElement="Process_TestLastMilestoneCallActivity">
|
||||
<bpmn:incoming>Flow_06ih0jm</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0c0z9vd</bpmn:outgoing>
|
||||
</bpmn:callActivity>
|
||||
<bpmn:sequenceFlow id="Flow_062tiay" sourceRef="milestone_done_with_call_activity" targetRef="Event_12t9aet" />
|
||||
<bpmn:manualTask id="milestone_done_with_call_activity">
|
||||
<bpmn:incoming>Flow_0c0z9vd</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_062tiay</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_caw9m9m">
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1ef2bzh_di" bpmnElement="milestone_started">
|
||||
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_02y238p_di" bpmnElement="call_activity">
|
||||
<dc:Bounds x="420" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_12t9aet_di" bpmnElement="Event_12t9aet">
|
||||
<dc:Bounds x="672" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_10b7ooz_di" bpmnElement="milestone_done_with_call_activity">
|
||||
<dc:Bounds x="540" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_088o54g_di" bpmnElement="Flow_088o54g">
|
||||
<di:waypoint x="215" y="177" />
|
||||
<di:waypoint x="270" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_06ih0jm_di" bpmnElement="Flow_06ih0jm">
|
||||
<di:waypoint x="370" y="177" />
|
||||
<di:waypoint x="420" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0c0z9vd_di" bpmnElement="Flow_0c0z9vd">
|
||||
<di:waypoint x="520" y="177" />
|
||||
<di:waypoint x="540" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_062tiay_di" bpmnElement="Flow_062tiay">
|
||||
<di:waypoint x="640" y="177" />
|
||||
<di:waypoint x="672" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -0,0 +1,56 @@
|
|||
<?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:process id="Process_TestLastMilestoneCallActivity" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_0vc53yr</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0vc53yr" sourceRef="StartEvent_1" targetRef="in_call_activity" />
|
||||
<bpmn:intermediateThrowEvent id="in_call_activity" name="In Call Activity">
|
||||
<bpmn:incoming>Flow_0vc53yr</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1o2uee3</bpmn:outgoing>
|
||||
</bpmn:intermediateThrowEvent>
|
||||
<bpmn:sequenceFlow id="Flow_1o2uee3" sourceRef="in_call_activity" targetRef="milestone_in_call_activity" />
|
||||
<bpmn:manualTask id="milestone_in_call_activity">
|
||||
<bpmn:incoming>Flow_1o2uee3</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0jcfams</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:endEvent id="Event_00alhkl" name="Done with call activity">
|
||||
<bpmn:incoming>Flow_0jcfams</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0jcfams" sourceRef="milestone_in_call_activity" targetRef="Event_00alhkl" />
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_TestLastMilestoneCallActivity">
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_0q7znrl_di" bpmnElement="in_call_activity">
|
||||
<dc:Bounds x="272" y="159" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="256" y="202" width="69" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0vu3k30_di" bpmnElement="milestone_in_call_activity">
|
||||
<dc:Bounds x="350" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_00alhkl_di" bpmnElement="Event_00alhkl">
|
||||
<dc:Bounds x="492" y="159" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="476" y="202" width="69" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_0vc53yr_di" bpmnElement="Flow_0vc53yr">
|
||||
<di:waypoint x="215" y="177" />
|
||||
<di:waypoint x="272" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1o2uee3_di" bpmnElement="Flow_1o2uee3">
|
||||
<di:waypoint x="308" y="177" />
|
||||
<di:waypoint x="350" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0jcfams_di" bpmnElement="Flow_0jcfams">
|
||||
<di:waypoint x="450" y="177" />
|
||||
<di:waypoint x="492" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -501,6 +501,13 @@ class BaseTest:
|
|||
db.session.delete(process_instance_report)
|
||||
db.session.commit()
|
||||
|
||||
def complete_next_manual_task(self, processor: ProcessInstanceProcessor) -> None:
|
||||
user_task = processor.get_ready_user_tasks()[0]
|
||||
human_task = processor.process_instance_model.human_tasks[0]
|
||||
ProcessInstanceService.complete_form_task(
|
||||
processor, user_task, {}, processor.process_instance_model.process_initiator, human_task
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
def app_config_mock(self, app: Flask, config_identifier: str, new_config_value: Any) -> Generator:
|
||||
initial_value = app.config[config_identifier]
|
||||
|
|
|
@ -202,6 +202,7 @@ class TestProcessApi(BaseTest):
|
|||
"start_in_seconds",
|
||||
"end_in_seconds",
|
||||
"process_initiator_username",
|
||||
"last_milestone_bpmn_name",
|
||||
"status",
|
||||
"summary",
|
||||
"description",
|
||||
|
@ -2844,6 +2845,7 @@ class TestProcessApi(BaseTest):
|
|||
assert response.json["results"][0]["id"] == process_instance.id
|
||||
assert response.json["results"][0]["key1"] == "value1"
|
||||
assert response.json["results"][0]["key2"] == "value2"
|
||||
assert response.json["results"][0]["last_milestone_bpmn_name"] == "Completed"
|
||||
assert response.json["pagination"]["count"] == 1
|
||||
assert response.json["pagination"]["pages"] == 1
|
||||
assert response.json["pagination"]["total"] == 1
|
||||
|
@ -3037,6 +3039,7 @@ class TestProcessApi(BaseTest):
|
|||
"accessor": "process_initiator_username",
|
||||
"filterable": False,
|
||||
},
|
||||
{"Header": "Last milestone", "accessor": "last_milestone_bpmn_name", "filterable": False},
|
||||
{"Header": "Status", "accessor": "status", "filterable": False},
|
||||
{"Header": "Task", "accessor": "task_title", "filterable": False},
|
||||
{"Header": "Waiting for", "accessor": "waiting_for", "filterable": False},
|
||||
|
@ -3055,6 +3058,7 @@ class TestProcessApi(BaseTest):
|
|||
"start_in_seconds",
|
||||
"end_in_seconds",
|
||||
"process_initiator_username",
|
||||
"last_milestone_bpmn_name",
|
||||
"status",
|
||||
"task_title",
|
||||
"waiting_for",
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
from flask import Flask
|
||||
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
|
||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||
|
||||
|
||||
class TestWorkflowExecutionService(BaseTest):
|
||||
def test_saves_last_milestone_appropriately(
|
||||
self,
|
||||
app: Flask,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
) -> None:
|
||||
process_model = load_test_spec(
|
||||
"test_group/test-last-milestone",
|
||||
process_model_source_directory="test-last-milestone",
|
||||
)
|
||||
process_instance = self.create_process_instance_from_process_model(process_model)
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.do_engine_steps(save=True, execution_strategy_name="greedy")
|
||||
assert process_instance.last_milestone_bpmn_name == "Started"
|
||||
|
||||
self.complete_next_manual_task(processor)
|
||||
assert process_instance.last_milestone_bpmn_name == "In Call Activity"
|
||||
self.complete_next_manual_task(processor)
|
||||
assert process_instance.last_milestone_bpmn_name == "Done with call activity"
|
||||
self.complete_next_manual_task(processor)
|
||||
assert process_instance.last_milestone_bpmn_name == "Completed"
|
||||
assert process_instance.status == "complete"
|
|
@ -42,6 +42,7 @@ import {
|
|||
convertSecondsToFormattedDateTime,
|
||||
convertSecondsToFormattedTimeHoursMinutes,
|
||||
getKeyByValue,
|
||||
getLastMilestoneFromProcessInstance,
|
||||
getPageInfoFromSearchParams,
|
||||
modifyProcessIdentifierForPathParam,
|
||||
refreshAtInterval,
|
||||
|
@ -1602,7 +1603,7 @@ export default function ProcessInstanceListTable({
|
|||
}
|
||||
return <span title={fullUsernameString}>{shortUsernameString}</span>;
|
||||
};
|
||||
const formatProcessInstanceId = (row: ProcessInstance, id: number) => {
|
||||
const formatProcessInstanceId = (_row: ProcessInstance, id: number) => {
|
||||
return <span data-qa="paginated-entity-id">{id}</span>;
|
||||
};
|
||||
const formatProcessModelIdentifier = (_row: any, identifier: any) => {
|
||||
|
@ -1611,6 +1612,16 @@ export default function ProcessInstanceListTable({
|
|||
const formatProcessModelDisplayName = (_row: any, identifier: any) => {
|
||||
return <span>{identifier}</span>;
|
||||
};
|
||||
const formatLastMilestone = (
|
||||
processInstance: ProcessInstance,
|
||||
value: any
|
||||
) => {
|
||||
const [valueToUse, truncatedValue] = getLastMilestoneFromProcessInstance(
|
||||
processInstance,
|
||||
value
|
||||
);
|
||||
return <span title={valueToUse}>{truncatedValue}</span>;
|
||||
};
|
||||
|
||||
const formatSecondsForDisplay = (_row: any, seconds: any) => {
|
||||
return convertSecondsToFormattedDateTime(seconds) || '-';
|
||||
|
@ -1629,6 +1640,7 @@ export default function ProcessInstanceListTable({
|
|||
end_in_seconds: formatSecondsForDisplay,
|
||||
updated_at_in_seconds: formatSecondsForDisplay,
|
||||
task_updated_at_in_seconds: formatSecondsForDisplay,
|
||||
last_milestone_bpmn_name: formatLastMilestone,
|
||||
};
|
||||
const columnAccessor = column.accessor as keyof ProcessInstance;
|
||||
const formatter =
|
||||
|
|
|
@ -46,7 +46,9 @@ export default function ProcessInstanceLogList({
|
|||
processInstanceId,
|
||||
}: OwnProps) {
|
||||
const [clearAll, setClearAll] = useState<boolean>(false);
|
||||
const [processInstanceLogs, setProcessInstanceLogs] = useState([]);
|
||||
const [processInstanceLogs, setProcessInstanceLogs] = useState<
|
||||
ProcessInstanceLogEntry[]
|
||||
>([]);
|
||||
const [pagination, setPagination] = useState(null);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
DATE_FORMAT,
|
||||
TIME_FORMAT_HOURS_MINUTES,
|
||||
} from './config';
|
||||
import { ProcessInstance } from './interfaces';
|
||||
|
||||
export const DEFAULT_PER_PAGE = 50;
|
||||
export const DEFAULT_PAGE = 1;
|
||||
|
@ -365,3 +366,30 @@ const FOUR_HOURS_IN_SECONDS = SECONDS_IN_HOUR * 4;
|
|||
|
||||
export const REFRESH_INTERVAL_SECONDS = 5;
|
||||
export const REFRESH_TIMEOUT_SECONDS = FOUR_HOURS_IN_SECONDS;
|
||||
|
||||
export const getLastMilestoneFromProcessInstance = (
|
||||
processInstance: ProcessInstance,
|
||||
value: any
|
||||
) => {
|
||||
let valueToUse = value;
|
||||
if (!valueToUse) {
|
||||
if (processInstance.status === 'not_started') {
|
||||
valueToUse = 'Created';
|
||||
} else if (
|
||||
['complete', 'error', 'terminated'].includes(processInstance.status)
|
||||
) {
|
||||
valueToUse = 'Completed';
|
||||
} else {
|
||||
valueToUse = 'Started';
|
||||
}
|
||||
}
|
||||
let truncatedValue = valueToUse;
|
||||
const milestoneLengthLimit = 20;
|
||||
if (truncatedValue.length > milestoneLengthLimit) {
|
||||
truncatedValue = `${truncatedValue.substring(
|
||||
0,
|
||||
milestoneLengthLimit - 3
|
||||
)}...`;
|
||||
}
|
||||
return [valueToUse, truncatedValue];
|
||||
};
|
||||
|
|
|
@ -196,7 +196,7 @@ dl {
|
|||
dl dt {
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
min-width: 5rem;
|
||||
min-width: 6rem;
|
||||
color: #161616;
|
||||
}
|
||||
|
||||
|
|
|
@ -172,6 +172,7 @@ export interface ProcessInstance {
|
|||
bpmn_version_control_type: string;
|
||||
process_metadata?: ProcessInstanceMetadata[];
|
||||
process_model_with_diagram_identifier?: string;
|
||||
last_milestone_bpmn_name?: string;
|
||||
|
||||
// from tasks
|
||||
potential_owner_usernames?: string;
|
||||
|
|
|
@ -42,6 +42,7 @@ import HttpService from '../services/HttpService';
|
|||
import ReactDiagramEditor from '../components/ReactDiagramEditor';
|
||||
import {
|
||||
convertSecondsToFormattedDateTime,
|
||||
getLastMilestoneFromProcessInstance,
|
||||
HUMAN_TASK_TYPES,
|
||||
modifyProcessIdentifierForPathParam,
|
||||
unModifyProcessIdentifierForPathParam,
|
||||
|
@ -322,7 +323,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
if (!processInstance) {
|
||||
return null;
|
||||
}
|
||||
let lastUpdatedTimeLabel = 'Updated at';
|
||||
let lastUpdatedTimeLabel = 'Updated';
|
||||
let lastUpdatedTime = processInstance.task_updated_at_in_seconds;
|
||||
if (processInstance.end_in_seconds) {
|
||||
lastUpdatedTimeLabel = 'Completed';
|
||||
|
@ -351,6 +352,12 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
statusColor = 'red';
|
||||
}
|
||||
|
||||
const [lastMilestoneFullValue, lastMilestoneTruncatedValue] =
|
||||
getLastMilestoneFromProcessInstance(
|
||||
processInstance,
|
||||
processInstance.last_milestone_bpmn_name
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid condensed fullWidth>
|
||||
<Column sm={4} md={4} lg={5}>
|
||||
|
@ -394,6 +401,12 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
</dd>
|
||||
</dl>
|
||||
{lastUpdatedTimeTag}
|
||||
<dl>
|
||||
<dt>Last milestone:</dt>
|
||||
<dd title={lastMilestoneFullValue}>
|
||||
{lastMilestoneTruncatedValue}
|
||||
</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>Revision:</dt>
|
||||
<dd>
|
||||
|
|
Loading…
Reference in New Issue