From the logs, allow viewing a diagram in a previous state (#15)

Co-authored-by: Elizabeth Esswein <elizabeth.esswein@gmail.com>
This commit is contained in:
jbirddog 2022-11-02 14:34:59 -04:00 committed by GitHub
parent 1579fb177d
commit 76fac5fb6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 155 additions and 17 deletions

View File

@ -1,8 +1,8 @@
"""empty message
Revision ID: bdd1d64689db
Revision ID: b1647eff45c9
Revises:
Create Date: 2022-11-02 11:31:50.606843
Create Date: 2022-11-02 14:25:09.992800
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'bdd1d64689db'
revision = 'b1647eff45c9'
down_revision = None
branch_labels = None
depends_on = None
@ -106,6 +106,7 @@ def upgrade():
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_identifier', sa.String(length=255), nullable=True),
sa.Column('spiff_step', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['process_initiator_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
@ -229,10 +230,22 @@ def upgrade():
sa.Column('timestamp', sa.DECIMAL(precision=17, scale=6), nullable=False),
sa.Column('message', sa.String(length=255), nullable=True),
sa.Column('current_user_id', sa.Integer(), nullable=True),
sa.Column('spiff_step', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['current_user_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ),
sa.PrimaryKeyConstraint('id')
)
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('timestamp', sa.DECIMAL(precision=17, scale=6), nullable=False),
sa.Column('completed_by_user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['completed_by_user_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('active_task_user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('active_task_id', sa.Integer(), nullable=False),
@ -266,6 +279,7 @@ def downgrade():
op.drop_index(op.f('ix_active_task_user_user_id'), table_name='active_task_user')
op.drop_index(op.f('ix_active_task_user_active_task_id'), table_name='active_task_user')
op.drop_table('active_task_user')
op.drop_table('spiff_step_details')
op.drop_table('spiff_logging')
op.drop_table('permission_assignment')
op.drop_table('message_instance')

View File

@ -1000,6 +1000,12 @@ paths:
description: If true, this wil return all tasks associated with the process instance and not just user tasks.
schema:
type: boolean
- name: spiff_step
in: query
required: false
description: If set will return the tasks as they were during a specific step of execution.
schema:
type: integer
get:
tags:
- Process Instances

View File

@ -46,6 +46,9 @@ from spiffworkflow_backend.models.process_instance_report import (
from spiffworkflow_backend.models.refresh_token import RefreshTokenModel # noqa: F401
from spiffworkflow_backend.models.secret_model import SecretModel # noqa: F401
from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel # 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.group import GroupModel # noqa: F401

View File

@ -81,6 +81,7 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
spiff_logs = relationship("SpiffLoggingModel", cascade="delete") # type: ignore
message_instances = relationship("MessageInstanceModel", cascade="delete") # type: ignore
message_correlations = relationship("MessageCorrelationModel", cascade="delete") # type: ignore
spiff_step_details = relationship("SpiffStepDetailsModel", cascade="delete") # type: ignore
bpmn_json: str | None = deferred(db.Column(db.JSON)) # type: ignore
start_in_seconds: int | None = db.Column(db.Integer)
@ -92,6 +93,7 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
bpmn_xml_file_contents: bytes | None = None
bpmn_version_control_type: str = db.Column(db.String(50))
bpmn_version_control_identifier: str = db.Column(db.String(255))
spiff_step: int = db.Column(db.Integer)
@property
def serialized(self) -> dict[str, Any]:

View File

@ -25,3 +25,4 @@ class SpiffLoggingModel(SpiffworkflowBaseDBModel):
timestamp: float = db.Column(db.DECIMAL(17, 6), nullable=False)
message: Optional[str] = db.Column(db.String(255), nullable=True)
current_user_id: int = db.Column(ForeignKey(UserModel.id), nullable=True)
spiff_step: int = db.Column(db.Integer, nullable=False)

View File

@ -0,0 +1,23 @@
"""Spiff_step_details."""
from dataclasses import dataclass
from flask_bpmn.models.db import db
from flask_bpmn.models.db import SpiffworkflowBaseDBModel
from sqlalchemy import ForeignKey
from sqlalchemy.orm import deferred
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.user import UserModel
@dataclass
class SpiffStepDetailsModel(SpiffworkflowBaseDBModel):
"""SpiffStepDetailsModel."""
__tablename__ = "spiff_step_details"
id: int = db.Column(db.Integer, primary_key=True)
process_instance_id: int = db.Column(ForeignKey(ProcessInstanceModel.id), nullable=False) # type: ignore
spiff_step: int = db.Column(db.Integer, nullable=False)
task_json: str | None = deferred(db.Column(db.JSON, nullable=False)) # type: ignore
timestamp: float = db.Column(db.DECIMAL(17, 6), nullable=False)
completed_by_user_id: int = db.Column(ForeignKey(UserModel.id), nullable=True)

View File

@ -56,6 +56,7 @@ from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema
from spiffworkflow_backend.models.secret_model import SecretModel
from spiffworkflow_backend.models.secret_model import SecretModelSchema
from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel
from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.routes.user import verify_token
from spiffworkflow_backend.services.authorization_service import AuthorizationService
@ -954,10 +955,23 @@ def task_list_my_tasks(page: int = 1, per_page: int = 100) -> flask.wrappers.Res
def process_instance_task_list(
process_instance_id: int, all_tasks: bool = False
process_instance_id: int, all_tasks: bool = False, spiff_step: int = 0
) -> flask.wrappers.Response:
"""Process_instance_task_list."""
process_instance = find_process_instance_by_id_or_raise(process_instance_id)
if spiff_step > 0:
step_detail = (
db.session.query(SpiffStepDetailsModel)
.filter(
SpiffStepDetailsModel.process_instance_id == process_instance.id,
SpiffStepDetailsModel.spiff_step == spiff_step,
)
.first()
)
if step_detail is not None:
process_instance.bpmn_json = json.dumps(step_detail.task_json)
processor = ProcessInstanceProcessor(process_instance)
spiff_tasks = None
@ -1233,6 +1247,7 @@ def script_unit_test_run(
"""Script_unit_test_run."""
# 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"].spiff_step = None
python_script = _get_required_parameter_or_raise("python_script", body)
input_json = _get_required_parameter_or_raise("input_json", body)

View File

@ -108,6 +108,8 @@ class SpiffFilter(logging.Filter):
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
@ -204,6 +206,11 @@ class DBHandler(logging.Handler):
timestamp = record.created
message = record.msg if hasattr(record, "msg") else None
current_user_id = record.current_user_id if hasattr(record, "current_user_id") else None # type: ignore
spiff_step = (
record.spiff_step # type: ignore
if hasattr(record, "spiff_step") and record.spiff_step is not None # type: ignore
else 1
)
spiff_log = SpiffLoggingModel(
process_instance_id=record.process_instance_id, # type: ignore
bpmn_process_identifier=bpmn_process_identifier,
@ -214,6 +221,7 @@ class DBHandler(logging.Handler):
message=message,
timestamp=timestamp,
current_user_id=current_user_id,
spiff_step=spiff_step,
)
db.session.add(spiff_log)
db.session.commit()

View File

@ -79,6 +79,7 @@ from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
)
from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user import UserModelSchema
from spiffworkflow_backend.scripts.script import Script
@ -276,9 +277,9 @@ class ProcessInstanceProcessor:
self, process_instance_model: ProcessInstanceModel, validate_only: bool = False
) -> None:
"""Create a Workflow Processor based on the serialized information available in the process_instance model."""
current_app.config[
"THREAD_LOCAL_DATA"
].process_instance_id = process_instance_model.id
tld = current_app.config["THREAD_LOCAL_DATA"]
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
current_app.config["THREAD_LOCAL_DATA"].process_model_identifier = (
@ -411,10 +412,8 @@ class ProcessInstanceProcessor:
bpmn_process_spec, subprocesses
)
def add_user_info_to_process_instance(
self, bpmn_process_instance: BpmnWorkflow
) -> None:
"""Add_user_info_to_process_instance."""
def current_user(self) -> Any:
"""Current_user."""
current_user = None
if UserService.has_user():
current_user = UserService.current_user()
@ -425,6 +424,14 @@ class ProcessInstanceProcessor:
elif self.process_instance_model.process_initiator_id:
current_user = self.process_instance_model.process_initiator
return current_user
def add_user_info_to_process_instance(
self, bpmn_process_instance: BpmnWorkflow
) -> None:
"""Add_user_info_to_process_instance."""
current_user = self.current_user()
if current_user:
current_user_data = UserModelSchema().dump(current_user)
tasks = bpmn_process_instance.get_tasks(TaskState.READY)
@ -542,9 +549,32 @@ class ProcessInstanceProcessor:
"lane_assignment_id": lane_assignment_id,
}
def save_spiff_step_details(self, bpmn_json: Optional[str]) -> None:
"""SaveSpiffStepDetails."""
if bpmn_json is None:
return
wf_json = json.loads(bpmn_json)
task_json = "{}"
if "tasks" in wf_json:
task_json = json.dumps(wf_json["tasks"])
# TODO want to just save the tasks, something wasn't immediately working
# so after the flow works with the full wf_json revisit this
task_json = wf_json
details_model = SpiffStepDetailsModel(
process_instance_id=self.process_instance_model.id,
spiff_step=self.process_instance_model.spiff_step or 1,
task_json=task_json,
timestamp=round(time.time()),
completed_by_user_id=self.current_user().id,
)
db.session.add(details_model)
db.session.commit()
def save(self) -> None:
"""Saves the current state of this processor to the database."""
self.process_instance_model.bpmn_json = self.serialize()
complete_states = [TaskState.CANCELLED, TaskState.COMPLETED]
user_tasks = list(self.get_all_user_tasks())
self.process_instance_model.status = self.get_status().value
@ -930,8 +960,19 @@ class ProcessInstanceProcessor:
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)
db.session.commit()
def do_engine_steps(self, exit_at: None = None, save: bool = False) -> None:
"""Do_engine_steps."""
self.increment_spiff_step()
try:
self.bpmn_process_instance.refresh_waiting_tasks()
self.bpmn_process_instance.do_engine_steps(exit_at=exit_at)
@ -944,6 +985,10 @@ class ProcessInstanceProcessor:
finally:
if save:
self.save()
bpmn_json = self.process_instance_model.bpmn_json
else:
bpmn_json = self.serialize()
self.save_spiff_step_details(bpmn_json)
def cancel_notify(self) -> None:
"""Cancel_notify."""
@ -1054,7 +1099,10 @@ class ProcessInstanceProcessor:
def complete_task(self, task: SpiffTask) -> None:
"""Complete_task."""
self.increment_spiff_step()
self.bpmn_process_instance.complete_task_from_id(task.id)
bpmn_json = self.serialize()
self.save_spiff_step_details(bpmn_json)
def get_data(self) -> dict[str, Any]:
"""Get_data."""

View File

@ -36,6 +36,7 @@ class TestSpiffLogging(BaseTest):
bpmn_task_identifier=bpmn_task_identifier,
message=message,
timestamp=timestamp,
spiff_step=1,
)
assert spiff_log.timestamp == timestamp

View File

@ -76,6 +76,10 @@ export default function AdminRoutes() {
path="process-models/:process_group_id/:process_model_id/process-instances/:process_instance_id"
element={<ProcessInstanceShow />}
/>
<Route
path="process-models/:process_group_id/:process_model_id/process-instances/:process_instance_id/:spiff_step"
element={<ProcessInstanceShow />}
/>
<Route
path="process-models/:process_group_id/:process_model_id/process-instances/reports"
element={<ProcessInstanceReportList />}

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { Table } from 'react-bootstrap';
import { useParams, useSearchParams } from 'react-router-dom';
import { useParams, useSearchParams, Link } from 'react-router-dom';
import PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import {
@ -39,7 +39,14 @@ export default function ProcessInstanceLogList() {
<td>{rowToUse.bpmn_task_name}</td>
<td>{rowToUse.bpmn_task_type}</td>
<td>{rowToUse.username}</td>
<td>{convertSecondsToFormattedDate(rowToUse.timestamp)}</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/${rowToUse.process_instance_id}/${rowToUse.spiff_step}`}
>
{convertSecondsToFormattedDate(rowToUse.timestamp)}
</Link>
</td>
</tr>
);
});

View File

@ -32,10 +32,16 @@ export default function ProcessInstanceShow() {
path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/${params.process_instance_id}`,
successCallback: setProcessInstance,
});
HttpService.makeCallToBackend({
path: `/process-instance/${params.process_instance_id}/tasks?all_tasks=true`,
successCallback: setTasks,
});
if (typeof params.spiff_step === 'undefined')
HttpService.makeCallToBackend({
path: `/process-instance/${params.process_instance_id}/tasks?all_tasks=true`,
successCallback: setTasks,
});
else
HttpService.makeCallToBackend({
path: `/process-instance/${params.process_instance_id}/tasks?all_tasks=true&spiff_step=${params.spiff_step}`,
successCallback: setTasks,
});
}, [params]);
const deleteProcessInstance = () => {