Merge branch 'main' into feature/git-integration

# Conflicts:
#	spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py
This commit is contained in:
mike cullerton 2022-12-06 16:27:36 -05:00
commit f580cadb21
22 changed files with 771 additions and 247 deletions

View File

@ -1,5 +1,5 @@
pip==22.2.2
nox==2022.8.7
nox-poetry==1.0.1
nox==2022.11.21
nox-poetry==1.0.2
poetry==1.2.2
virtualenv==20.16.5

View File

@ -1,8 +1,8 @@
"""empty message
Revision ID: ff1c1628337c
Revision ID: 40a2ed63cc5a
Revises:
Create Date: 2022-11-28 15:08:52.014254
Create Date: 2022-11-29 16:59:02.980181
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ff1c1628337c'
revision = '40a2ed63cc5a'
down_revision = None
branch_labels = None
depends_on = None
@ -249,6 +249,7 @@ def upgrade():
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('process_instance_id', 'key', name='process_instance_metadata_unique')
)
op.create_index(op.f('ix_process_instance_metadata_key'), 'process_instance_metadata', ['key'], unique=False)
op.create_table('spiff_step_details',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('process_instance_id', sa.Integer(), nullable=False),
@ -295,6 +296,7 @@ def downgrade():
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_index(op.f('ix_process_instance_metadata_key'), table_name='process_instance_metadata')
op.drop_table('process_instance_metadata')
op.drop_table('permission_assignment')
op.drop_table('message_instance')

13
poetry.lock generated
View File

@ -1851,7 +1851,7 @@ lxml = "*"
type = "git"
url = "https://github.com/sartography/SpiffWorkflow"
reference = "main"
resolved_reference = "062eaf15d28c66f8cf07f68409429560251b12c7"
resolved_reference = "ffb1686757f944065580dd2db8def73d6c1f0134"
[[package]]
name = "SQLAlchemy"
@ -2989,7 +2989,18 @@ psycopg2 = [
{file = "psycopg2-2.9.4.tar.gz", hash = "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f"},
]
pyasn1 = [
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
]
pycodestyle = [

View File

@ -338,9 +338,9 @@ paths:
schema:
$ref: "#/components/schemas/ProcessModel"
/process-models/{modified_process_model_id}/files:
/process-models/{modified_process_model_identifier}/files:
parameters:
- name: modified_process_model_id
- name: modified_process_model_identifier
in: path
required: true
description: The process_model_id, modified to replace slashes (/)
@ -570,6 +570,12 @@ paths:
description: Specifies the identifier of a report to use, if any
schema:
type: string
- name: report_id
in: query
required: false
description: Specifies the identifier of a report to use, if any
schema:
type: integer
get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_list
summary: Returns a list of process instances for a given process model
@ -585,33 +591,6 @@ paths:
items:
$ref: "#/components/schemas/Workflow"
/process-instances/{process_instance_id}/task/{task_id}/update:
parameters:
- name: process_instance_id
in: path
required: true
description: The unique id of the process instance
schema:
type: string
- name: task_id
in: path
required: true
description: The unique id of the task
schema:
type: string
post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.update_task_data
summary: Update the task data for requested instance and task
tags:
- Process Instances
responses:
"200":
description: Task Updated Successfully
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/process-models/{process_group_id}/{process_model_id}/script-unit-tests:
parameters:
- name: process_group_id
@ -666,15 +645,14 @@ paths:
schema:
$ref: "#/components/schemas/Workflow"
/process-models/{modified_process_model_id}/process-instances:
/process-instances/{modified_process_model_identifier}:
parameters:
- name: modified_process_model_id
- name: modified_process_model_identifier
in: path
required: true
description: The unique id of an existing process model.
schema:
type: string
# process_instance_create
post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_create
summary: Creates an process instance from a process model and returns the instance
@ -688,28 +666,7 @@ paths:
schema:
$ref: "#/components/schemas/Workflow"
/process-instances/{process_instance_id}:
parameters:
- name: process_instance_id
in: path
required: true
description: The unique id of an existing process instance.
schema:
type: integer
delete:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_delete
summary: Deletes a single process instance
tags:
- Process Instances
responses:
"200":
description: The process instance was deleted.
content:
application/json:
schema:
$ref: "#/components/schemas/OkTrue"
/process-models/{modified_process_model_identifier}/process-instances/{process_instance_id}:
/process-instances/{modified_process_model_identifier}/{process_instance_id}:
parameters:
- name: modified_process_model_identifier
in: path
@ -735,6 +692,18 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
delete:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_delete
summary: Deletes a single process instance
tags:
- Process Instances
responses:
"200":
description: The process instance was deleted.
content:
application/json:
schema:
$ref: "#/components/schemas/OkTrue"
/process-instances/{modified_process_model_identifier}/{process_instance_id}/run:
parameters:
@ -763,7 +732,7 @@ paths:
schema:
$ref: "#/components/schemas/Workflow"
/process-instances/{process_instance_id}/terminate:
/process-instances/{modified_process_model_identifier}/{process_instance_id}/terminate:
parameters:
- name: process_instance_id
in: path
@ -784,7 +753,7 @@ paths:
schema:
$ref: "#/components/schemas/OkTrue"
/process-instances/{process_instance_id}/suspend:
/process-instances/{modified_process_model_identifier}/{process_instance_id}/suspend:
parameters:
- name: process_instance_id
in: path
@ -805,7 +774,7 @@ paths:
schema:
$ref: "#/components/schemas/OkTrue"
/process-instances/{process_instance_id}/resume:
/process-instances/{modified_process_model_identifier}/{process_instance_id}/resume:
parameters:
- name: process_instance_id
in: path
@ -867,14 +836,30 @@ paths:
schema:
$ref: "#/components/schemas/OkTrue"
/process-instances/reports/{report_identifier}:
/process-instances/reports/columns:
get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_report_column_list
summary: Returns all available columns for a process instance report.
tags:
- Process Instances
responses:
"200":
description: Workflow.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Workflow"
/process-instances/reports/{report_id}:
parameters:
- name: report_identifier
- name: report_id
in: path
required: true
description: The unique id of an existing report
schema:
type: string
type: integer
- name: page
in: query
required: false
@ -926,9 +911,9 @@ paths:
schema:
$ref: "#/components/schemas/OkTrue"
/process-models/{modified_process_model_id}/files/{file_name}:
/process-models/{modified_process_model_identifier}/files/{file_name}:
parameters:
- name: modified_process_model_id
- name: modified_process_model_identifier
in: path
required: true
description: The modified process model id
@ -1105,9 +1090,9 @@ paths:
items:
$ref: "#/components/schemas/Task"
/process-instances/{modified_process_model_id}/{process_instance_id}/tasks:
/task-data/{modified_process_model_identifier}/{process_instance_id}:
parameters:
- name: modified_process_model_id
- name: modified_process_model_identifier
in: path
required: true
description: The modified id of an existing process model
@ -1146,11 +1131,44 @@ paths:
items:
$ref: "#/components/schemas/Task"
/service_tasks:
/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
put:
operationId: spiffworkflow_backend.routes.process_api_blueprint.update_task_data
summary: Update the task data for requested instance and task
tags:
- Process Instances
responses:
"200":
description: Task Updated Successfully
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/service-tasks:
get:
tags:
- Service Tasks
operationId: spiffworkflow_backend.routes.process_api_blueprint.service_tasks_show
operationId: spiffworkflow_backend.routes.process_api_blueprint.service_task_list
summary: Gets all available service task connectors
responses:
"200":
@ -1330,7 +1348,7 @@ paths:
schema:
$ref: "#/components/schemas/Workflow"
/process-instances/{process_instance_id}/logs:
/logs/{modified_process_model_identifier}/{process_instance_id}:
parameters:
- name: process_instance_id
in: path
@ -1350,6 +1368,12 @@ paths:
description: The number of items to show per page. Defaults to page 10.
schema:
type: integer
- name: detailed
in: query
required: false
description: Show the detailed view, which includes all log entries
schema:
type: boolean
get:
tags:
- Process Instances

View File

@ -12,7 +12,6 @@ groups:
mike,
jason,
j,
amir,
jarrad,
elizabeth,
jon,
@ -70,6 +69,12 @@ permissions:
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/service-tasks
# read all for everybody
read-all-process-groups:
@ -98,6 +103,12 @@ permissions:
allowed_permissions: [read]
uri: /v1.0/processes
task-data-read:
groups: [demo]
users: []
allowed_permissions: [read]
uri: /v1.0/task-data/*
manage-procurement-admin:
groups: ["Project Lead"]
@ -170,17 +181,17 @@ permissions:
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:*
core1-admin-models-instantiate:
groups: ["core-contributor"]
groups: ["core-contributor", "Finance Team"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-models/misc:category_number_one:process-model-with-form/process-instances
core1-admin-instances:
groups: ["core-contributor"]
groups: ["core-contributor", "Finance Team"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form:*
core1-admin-instances-slash:
groups: ["core-contributor"]
groups: ["core-contributor", "Finance Team"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form/*

View File

@ -12,7 +12,6 @@ groups:
mike,
jason,
j,
amir,
jarrad,
elizabeth,
jon,
@ -98,6 +97,12 @@ permissions:
allowed_permissions: [read]
uri: /v1.0/processes
task-data-read:
groups: [demo]
users: []
allowed_permissions: [read]
uri: /v1.0/task-data/*
manage-procurement-admin:
groups: ["Project Lead"]

View File

@ -23,7 +23,7 @@ class ProcessInstanceMetadataModel(SpiffworkflowBaseDBModel):
process_instance_id: int = db.Column(
ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore
)
key: str = db.Column(db.String(255), nullable=False)
key: str = db.Column(db.String(255), nullable=False, index=True)
value: str = db.Column(db.String(255), nullable=False)
updated_at_in_seconds: int = db.Column(db.Integer, nullable=False)

View File

@ -26,6 +26,10 @@ from spiffworkflow_backend.services.process_instance_processor import (
ReportMetadata = dict[str, Any]
class ProcessInstanceReportAlreadyExistsError(Exception):
"""ProcessInstanceReportAlreadyExistsError."""
class ProcessInstanceReportResult(TypedDict):
"""ProcessInstanceReportResult."""
@ -63,7 +67,7 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
),
)
id = db.Column(db.Integer, primary_key=True)
id: int = db.Column(db.Integer, primary_key=True)
identifier: str = db.Column(db.String(50), nullable=False, index=True)
report_metadata: dict = deferred(db.Column(db.JSON)) # type: ignore
created_by_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True)
@ -71,6 +75,11 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
created_at_in_seconds = db.Column(db.Integer)
updated_at_in_seconds = db.Column(db.Integer)
@classmethod
def default_order_by(cls) -> list[str]:
"""Default_order_by."""
return ["-start_in_seconds", "-id"]
@classmethod
def add_fixtures(cls) -> None:
"""Add_fixtures."""
@ -120,21 +129,27 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
identifier: str,
user: UserModel,
report_metadata: ReportMetadata,
) -> None:
) -> ProcessInstanceReportModel:
"""Make_fixture_report."""
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=identifier,
created_by_id=user.id,
).first()
if process_instance_report is None:
process_instance_report = cls(
identifier=identifier,
created_by_id=user.id,
report_metadata=report_metadata,
if process_instance_report is not None:
raise ProcessInstanceReportAlreadyExistsError(
f"Process instance report with identifier already exists: {identifier}"
)
db.session.add(process_instance_report)
db.session.commit()
process_instance_report = cls(
identifier=identifier,
created_by_id=user.id,
report_metadata=report_metadata,
)
db.session.add(process_instance_report)
db.session.commit()
return process_instance_report # type: ignore
@classmethod
def ticket_for_month_report(cls) -> dict:
@ -204,18 +219,8 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
user: UserModel,
) -> ProcessInstanceReportModel:
"""Create_with_attributes."""
# <<<<<<< HEAD
# process_model = ProcessModelService.get_process_model(
# process_model_id=f"{process_model_identifier}"
# )
# process_instance_report = cls(
# identifier=identifier,
# process_group_identifier="process_model.process_group_id",
# process_model_identifier=process_model.id,
# =======
process_instance_report = cls(
identifier=identifier,
# >>>>>>> main
created_by_id=user.id,
report_metadata=report_metadata,
)

View File

@ -38,6 +38,7 @@ class ProcessModelInfo:
fault_or_suspend_on_exception: str = NotificationType.fault.value
exception_notification_addresses: list[str] = field(default_factory=list)
parent_groups: list[dict] | None = None
metadata_extraction_paths: list[dict[str, str]] | None = None
def __post_init__(self) -> None:
"""__post_init__."""
@ -76,6 +77,13 @@ class ProcessModelInfoSchema(Schema):
exception_notification_addresses = marshmallow.fields.List(
marshmallow.fields.String
)
metadata_extraction_paths = marshmallow.fields.List(
marshmallow.fields.Dict(
keys=marshmallow.fields.Str(required=False),
values=marshmallow.fields.Str(required=False),
required=False,
)
)
@post_load
def make_spec(

View File

@ -1,6 +1,7 @@
"""APIs for dealing with process groups, process models, and process instances."""
import json
import random
import re
import string
import uuid
from typing import Any
@ -30,6 +31,8 @@ from SpiffWorkflow.task import TaskState
from sqlalchemy import and_
from sqlalchemy import asc
from sqlalchemy import desc
from sqlalchemy import func
from sqlalchemy.orm import aliased
from sqlalchemy.orm import joinedload
from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
@ -52,6 +55,9 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceApiSche
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_metadata import (
ProcessInstanceMetadataModel,
)
from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel,
)
@ -152,9 +158,9 @@ def modify_process_model_id(process_model_id: str) -> str:
return process_model_id.replace("/", ":")
def un_modify_modified_process_model_id(modified_process_model_id: str) -> str:
def un_modify_modified_process_model_id(modified_process_model_identifier: str) -> str:
"""Un_modify_modified_process_model_id."""
return modified_process_model_id.replace(":", "/")
return modified_process_model_identifier.replace(":", "/")
def process_group_add(body: dict) -> flask.wrappers.Response:
@ -256,19 +262,26 @@ def process_model_create(
modified_process_group_id: str, body: Dict[str, Union[str, bool, int]]
) -> flask.wrappers.Response:
"""Process_model_create."""
process_model_info = ProcessModelInfoSchema().load(body)
body_include_list = [
"id",
"display_name",
"primary_file_name",
"primary_process_id",
"description",
"metadata_extraction_paths",
]
body_filtered = {
include_item: body[include_item]
for include_item in body_include_list
if include_item in body
}
if modified_process_group_id is None:
raise ApiError(
error_code="process_group_id_not_specified",
message="Process Model could not be created when process_group_id path param is unspecified",
status_code=400,
)
if process_model_info is None:
raise ApiError(
error_code="process_model_could_not_be_created",
message=f"Process Model could not be created from given body: {body}",
status_code=400,
)
unmodified_process_group_id = un_modify_modified_process_model_id(
modified_process_group_id
@ -281,6 +294,14 @@ def process_model_create(
status_code=400,
)
process_model_info = ProcessModelInfo(**body_filtered) # type: ignore
if process_model_info is None:
raise ApiError(
error_code="process_model_could_not_be_created",
message=f"Process Model could not be created from given body: {body}",
status_code=400,
)
ProcessModelService.add_process_model(process_model_info)
return Response(
json.dumps(ProcessModelInfoSchema().dump(process_model_info)),
@ -294,7 +315,6 @@ def process_model_delete(
) -> flask.wrappers.Response:
"""Process_model_delete."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
# process_model_identifier = f"{process_group_id}/{process_model_id}"
ProcessModelService().process_model_delete(process_model_identifier)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
@ -309,6 +329,7 @@ def process_model_update(
"primary_file_name",
"primary_process_id",
"description",
"metadata_extraction_paths",
]
body_filtered = {
include_item: body[include_item]
@ -316,7 +337,6 @@ def process_model_update(
if include_item in body
}
# process_model_identifier = f"{process_group_id}/{process_model_id}"
process_model = get_process_model(process_model_identifier)
ProcessModelService.update_process_model(process_model, body_filtered)
return ProcessModelInfoSchema().dump(process_model)
@ -325,10 +345,7 @@ def process_model_update(
def process_model_show(modified_process_model_identifier: str) -> Any:
"""Process_model_show."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
# process_model_identifier = f"{process_group_id}/{process_model_id}"
process_model = get_process_model(process_model_identifier)
# TODO: Temporary. Should not need the next line once models have correct ids
# process_model.id = process_model_identifier
files = sorted(SpecFileService.get_files(process_model))
process_model.files = files
for file in process_model.files:
@ -405,9 +422,9 @@ def process_list() -> Any:
return SpecReferenceSchema(many=True).dump(references)
def get_file(modified_process_model_id: str, file_name: str) -> Any:
def get_file(modified_process_model_identifier: str, file_name: str) -> Any:
"""Get_file."""
process_model_identifier = modified_process_model_id.replace(":", "/")
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = get_process_model(process_model_identifier)
files = SpecFileService.get_files(process_model, file_name)
if len(files) == 0:
@ -427,11 +444,10 @@ def get_file(modified_process_model_id: str, file_name: str) -> Any:
def process_model_file_update(
modified_process_model_id: str, file_name: str
modified_process_model_identifier: str, file_name: str
) -> flask.wrappers.Response:
"""Process_model_file_update."""
process_model_identifier = modified_process_model_id.replace(":", "/")
# process_model_identifier = f"{process_group_id}/{process_model_id}"
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = get_process_model(process_model_identifier)
request_file = get_file_from_request()
@ -457,10 +473,10 @@ def process_model_file_update(
def process_model_file_delete(
modified_process_model_id: str, file_name: str
modified_process_model_identifier: str, file_name: str
) -> flask.wrappers.Response:
"""Process_model_file_delete."""
process_model_identifier = modified_process_model_id.replace(":", "/")
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = get_process_model(process_model_identifier)
try:
SpecFileService.delete_file(process_model, file_name)
@ -476,9 +492,9 @@ def process_model_file_delete(
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def add_file(modified_process_model_id: str) -> flask.wrappers.Response:
def add_file(modified_process_model_identifier: str) -> flask.wrappers.Response:
"""Add_file."""
process_model_identifier = modified_process_model_id.replace(":", "/")
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = get_process_model(process_model_identifier)
request_file = get_file_from_request()
if not request_file.filename:
@ -499,10 +515,12 @@ def add_file(modified_process_model_id: str) -> flask.wrappers.Response:
)
def process_instance_create(modified_process_model_id: str) -> flask.wrappers.Response:
def process_instance_create(
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Create_process_instance."""
process_model_identifier = un_modify_modified_process_model_id(
modified_process_model_id
modified_process_model_identifier
)
process_instance = (
ProcessInstanceService.create_process_instance_from_process_model_identifier(
@ -560,6 +578,7 @@ def process_instance_run(
def process_instance_terminate(
process_instance_id: int,
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_instance_run."""
process_instance = ProcessInstanceService().get_process_instance(
@ -572,6 +591,7 @@ def process_instance_terminate(
def process_instance_suspend(
process_instance_id: int,
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_instance_suspend."""
process_instance = ProcessInstanceService().get_process_instance(
@ -584,6 +604,7 @@ def process_instance_suspend(
def process_instance_resume(
process_instance_id: int,
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_instance_resume."""
process_instance = ProcessInstanceService().get_process_instance(
@ -595,19 +616,24 @@ def process_instance_resume(
def process_instance_log_list(
modified_process_model_identifier: str,
process_instance_id: int,
page: int = 1,
per_page: int = 100,
detailed: bool = False,
) -> flask.wrappers.Response:
"""Process_instance_log_list."""
# to make sure the process instance exists
process_instance = find_process_instance_by_id_or_raise(process_instance_id)
log_query = SpiffLoggingModel.query.filter(
SpiffLoggingModel.process_instance_id == process_instance.id
)
if not detailed:
log_query = log_query.filter(SpiffLoggingModel.message.in_(["State change to COMPLETED"])) # type: ignore
logs = (
SpiffLoggingModel.query.filter(
SpiffLoggingModel.process_instance_id == process_instance.id
)
.order_by(SpiffLoggingModel.timestamp.desc()) # type: ignore
log_query.order_by(SpiffLoggingModel.timestamp.desc()) # type: ignore
.join(
UserModel, UserModel.id == SpiffLoggingModel.current_user_id, isouter=True
) # isouter since if we don't have a user, we still want the log
@ -653,6 +679,7 @@ def message_instance_list(
.add_columns(
MessageModel.identifier.label("message_identifier"),
ProcessInstanceModel.process_model_identifier,
ProcessInstanceModel.process_model_display_name,
)
.paginate(page=page, per_page=per_page, error_out=False)
)
@ -787,10 +814,11 @@ def process_instance_list(
with_tasks_completed_by_my_group: Optional[bool] = None,
user_filter: Optional[bool] = False,
report_identifier: Optional[str] = None,
report_id: Optional[int] = None,
) -> flask.wrappers.Response:
"""Process_instance_list."""
process_instance_report = ProcessInstanceReportService.report_with_identifier(
g.user, report_identifier
g.user, report_id, report_identifier
)
if user_filter:
@ -821,7 +849,6 @@ def process_instance_list(
)
)
# process_model_identifier = un_modify_modified_process_model_id(modified_process_model_identifier)
process_instance_query = ProcessInstanceModel.query
# Always join that hot user table for good performance at serialization time.
process_instance_query = process_instance_query.options(
@ -939,25 +966,78 @@ def process_instance_list(
UserGroupAssignmentModel.user_id == g.user.id
)
instance_metadata_aliases = {}
stock_columns = ProcessInstanceReportService.get_column_names_for_model(
ProcessInstanceModel
)
for column in process_instance_report.report_metadata["columns"]:
if column["accessor"] in stock_columns:
continue
instance_metadata_alias = aliased(ProcessInstanceMetadataModel)
instance_metadata_aliases[column["accessor"]] = instance_metadata_alias
filter_for_column = None
if "filter_by" in process_instance_report.report_metadata:
filter_for_column = next(
(
f
for f in process_instance_report.report_metadata["filter_by"]
if f["field_name"] == column["accessor"]
),
None,
)
isouter = True
conditions = [
ProcessInstanceModel.id == instance_metadata_alias.process_instance_id,
instance_metadata_alias.key == column["accessor"],
]
if filter_for_column:
isouter = False
conditions.append(
instance_metadata_alias.value == filter_for_column["field_value"]
)
process_instance_query = process_instance_query.join(
instance_metadata_alias, and_(*conditions), isouter=isouter
).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"]))
order_by_query_array = []
order_by_array = process_instance_report.report_metadata["order_by"]
if len(order_by_array) < 1:
order_by_array = ProcessInstanceReportModel.default_order_by()
for order_by_option in order_by_array:
attribute = re.sub("^-", "", order_by_option)
if attribute in stock_columns:
if order_by_option.startswith("-"):
order_by_query_array.append(
getattr(ProcessInstanceModel, attribute).desc()
)
else:
order_by_query_array.append(
getattr(ProcessInstanceModel, attribute).asc()
)
elif attribute in instance_metadata_aliases:
if order_by_option.startswith("-"):
order_by_query_array.append(
instance_metadata_aliases[attribute].value.desc()
)
else:
order_by_query_array.append(
instance_metadata_aliases[attribute].value.asc()
)
process_instances = (
process_instance_query.group_by(ProcessInstanceModel.id)
.order_by(
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore
)
.add_columns(ProcessInstanceModel.id)
.order_by(*order_by_query_array)
.paginate(page=page, per_page=per_page, error_out=False)
)
results = list(
map(
ProcessInstanceService.serialize_flat_with_task_data,
process_instances.items,
)
results = ProcessInstanceReportService.add_metadata_columns_to_process_instance(
process_instances.items, process_instance_report.report_metadata["columns"]
)
report_metadata = process_instance_report.report_metadata
response_json = {
"report_identifier": process_instance_report.identifier,
"report_metadata": report_metadata,
"report": process_instance_report,
"results": results,
"filters": report_filter.to_dict(),
"pagination": {
@ -970,6 +1050,22 @@ def process_instance_list(
return make_response(jsonify(response_json), 200)
def process_instance_report_column_list() -> flask.wrappers.Response:
"""Process_instance_report_column_list."""
table_columns = ProcessInstanceReportService.builtin_column_options()
columns_for_metadata = (
db.session.query(ProcessInstanceMetadataModel.key)
.order_by(ProcessInstanceMetadataModel.key)
.distinct() # type: ignore
.all()
)
columns_for_metadata_strings = [
{"Header": i[0], "accessor": i[0], "filterable": True}
for i in columns_for_metadata
]
return make_response(jsonify(table_columns + columns_for_metadata_strings), 200)
def process_instance_show(
modified_process_model_identifier: str, process_instance_id: int
) -> flask.wrappers.Response:
@ -996,7 +1092,9 @@ def process_instance_show(
return make_response(jsonify(process_instance), 200)
def process_instance_delete(process_instance_id: int) -> flask.wrappers.Response:
def process_instance_delete(
process_instance_id: int, modified_process_model_identifier: str
) -> flask.wrappers.Response:
"""Create_process_instance."""
process_instance = find_process_instance_by_id_or_raise(process_instance_id)
@ -1026,22 +1124,22 @@ def process_instance_report_list(
def process_instance_report_create(body: Dict[str, Any]) -> flask.wrappers.Response:
"""Process_instance_report_create."""
ProcessInstanceReportModel.create_report(
process_instance_report = ProcessInstanceReportModel.create_report(
identifier=body["identifier"],
user=g.user,
report_metadata=body["report_metadata"],
)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
return make_response(jsonify(process_instance_report), 201)
def process_instance_report_update(
report_identifier: str,
report_id: int,
body: Dict[str, Any],
) -> flask.wrappers.Response:
"""Process_instance_report_create."""
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier,
id=report_id,
created_by_id=g.user.id,
).first()
if process_instance_report is None:
@ -1054,15 +1152,15 @@ def process_instance_report_update(
process_instance_report.report_metadata = body["report_metadata"]
db.session.commit()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
return make_response(jsonify(process_instance_report), 201)
def process_instance_report_delete(
report_identifier: str,
report_id: int,
) -> flask.wrappers.Response:
"""Process_instance_report_create."""
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier,
id=report_id,
created_by_id=g.user.id,
).first()
if process_instance_report is None:
@ -1078,11 +1176,9 @@ def process_instance_report_delete(
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def service_tasks_show() -> flask.wrappers.Response:
"""Service_tasks_show."""
def service_task_list() -> flask.wrappers.Response:
"""Service_task_list."""
available_connectors = ServiceTaskService.available_connectors()
print(available_connectors)
return Response(
json.dumps(available_connectors), status=200, mimetype="application/json"
)
@ -1116,19 +1212,17 @@ def authentication_callback(
def process_instance_report_show(
report_identifier: str,
report_id: int,
page: int = 1,
per_page: int = 100,
) -> flask.wrappers.Response:
"""Process_instance_list."""
process_instances = ProcessInstanceModel.query.order_by( # .filter_by(process_model_identifier=process_model.id)
"""Process_instance_report_show."""
process_instances = ProcessInstanceModel.query.order_by(
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore
).paginate(
page=page, per_page=per_page, error_out=False
)
).paginate(page=page, per_page=per_page, error_out=False)
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier,
id=report_id,
created_by_id=g.user.id,
).first()
if process_instance_report is None:
@ -1290,7 +1384,7 @@ def get_tasks(
def process_instance_task_list(
modified_process_model_id: str,
modified_process_model_identifier: str,
process_instance_id: int,
all_tasks: bool = False,
spiff_step: int = 0,
@ -1405,9 +1499,6 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
task.form_ui_schema = ui_form_contents
if task.properties and task.data and "instructionsForEndUser" in task.properties:
print(
f"task.properties['instructionsForEndUser']: {task.properties['instructionsForEndUser']}"
)
if task.properties["instructionsForEndUser"]:
task.properties["instructionsForEndUser"] = render_jinja_template(
task.properties["instructionsForEndUser"], task.data
@ -1854,7 +1945,12 @@ def _update_form_schema_with_task_data_as_needed(
_update_form_schema_with_task_data_as_needed(o, task_data)
def update_task_data(process_instance_id: str, task_id: str, body: Dict) -> Response:
def update_task_data(
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)

View File

@ -1,4 +1,4 @@
"""Get_env."""
"""Save process instance metadata."""
from typing import Any
from flask_bpmn.models.db import db

View File

@ -235,8 +235,9 @@ class AuthenticationService:
refresh_token_object: RefreshTokenModel = RefreshTokenModel.query.filter(
RefreshTokenModel.user_id == user_id
).first()
assert refresh_token_object # noqa: S101
return refresh_token_object.token
if refresh_token_object:
return refresh_token_object.token
return None
@classmethod
def get_auth_token_from_refresh_token(cls, refresh_token: str) -> dict:

View File

@ -81,6 +81,9 @@ from spiffworkflow_backend.models.message_instance import MessageInstanceModel
from spiffworkflow_backend.models.message_instance import MessageModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_instance_metadata import (
ProcessInstanceMetadataModel,
)
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
@ -178,7 +181,12 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
)
return Script.generate_augmented_list(script_attributes_context)
def evaluate(self, task: SpiffTask, expression: str, external_methods=None) -> Any:
def evaluate(
self,
task: SpiffTask,
expression: str,
external_methods: Optional[dict[str, Any]] = None,
) -> Any:
"""Evaluate."""
return self._evaluate(expression, task.data, task, external_methods)
@ -571,6 +579,36 @@ class ProcessInstanceProcessor:
db.session.add(details_model)
db.session.commit()
def extract_metadata(self, process_model_info: ProcessModelInfo) -> None:
"""Extract_metadata."""
metadata_extraction_paths = process_model_info.metadata_extraction_paths
if metadata_extraction_paths is None:
return
if len(metadata_extraction_paths) <= 0:
return
current_data = self.get_current_data()
for metadata_extraction_path in metadata_extraction_paths:
key = metadata_extraction_path["key"]
path = metadata_extraction_path["path"]
path_segments = path.split(".")
data_for_key = current_data
for path_segment in path_segments:
data_for_key = data_for_key[path_segment]
pim = ProcessInstanceMetadataModel.query.filter_by(
process_instance_id=self.process_instance_model.id,
key=key,
).first()
if pim is None:
pim = ProcessInstanceMetadataModel(
process_instance_id=self.process_instance_model.id,
key=key,
)
pim.value = data_for_key
db.session.add(pim)
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()
@ -597,6 +635,15 @@ class ProcessInstanceProcessor:
process_instance_id=self.process_instance_model.id
).all()
ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks()
process_model_display_name = ""
process_model_info = self.process_model_service.get_process_model(
self.process_instance_model.process_model_identifier
)
if process_model_info is not None:
process_model_display_name = process_model_info.display_name
self.extract_metadata(process_model_info)
for ready_or_waiting_task in ready_or_waiting_tasks:
# filter out non-usertasks
task_spec = ready_or_waiting_task.task_spec
@ -615,13 +662,6 @@ class ProcessInstanceProcessor:
if "formUiSchemaFilename" in properties:
ui_form_file_name = properties["formUiSchemaFilename"]
process_model_display_name = ""
process_model_info = self.process_model_service.get_process_model(
self.process_instance_model.process_model_identifier
)
if process_model_info is not None:
process_model_display_name = process_model_info.display_name
active_task = None
for at in active_tasks:
if at.task_id == str(ready_or_waiting_task.id):
@ -1146,8 +1186,8 @@ class ProcessInstanceProcessor:
def get_current_data(self) -> dict[str, Any]:
"""Get the current data for the process.
Return either most recent task data or the process data
if the process instance is complete
Return either the most recent task data or--if the process instance is complete--
the process data.
"""
if self.process_instance_model.status == "complete":
return self.get_data()

View File

@ -2,6 +2,9 @@
from dataclasses import dataclass
from typing import Optional
import sqlalchemy
from flask_bpmn.models.db import db
from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel,
)
@ -57,12 +60,21 @@ class ProcessInstanceReportService:
@classmethod
def report_with_identifier(
cls, user: UserModel, report_identifier: Optional[str] = None
cls,
user: UserModel,
report_id: Optional[int] = None,
report_identifier: Optional[str] = None,
) -> ProcessInstanceReportModel:
"""Report_with_filter."""
if report_id is not None:
process_instance_report = ProcessInstanceReportModel.query.filter_by(
id=report_id, created_by_id=user.id
).first()
if process_instance_report is not None:
return process_instance_report # type: ignore
if report_identifier is None:
report_identifier = "default"
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier, created_by_id=user.id
).first()
@ -73,17 +85,9 @@ class ProcessInstanceReportService:
# TODO replace with system reports that are loaded on launch (or similar)
temp_system_metadata_map = {
"default": {
"columns": [
{"Header": "id", "accessor": "id"},
{
"Header": "process_model_display_name",
"accessor": "process_model_display_name",
},
{"Header": "start_in_seconds", "accessor": "start_in_seconds"},
{"Header": "end_in_seconds", "accessor": "end_in_seconds"},
{"Header": "username", "accessor": "username"},
{"Header": "status", "accessor": "status"},
],
"columns": cls.builtin_column_options(),
"filter_by": [],
"order_by": ["-start_in_seconds", "-id"],
},
"system_report_instances_initiated_by_me": {
"columns": [
@ -97,48 +101,31 @@ class ProcessInstanceReportService:
{"Header": "status", "accessor": "status"},
],
"filter_by": [{"field_name": "initiated_by_me", "field_value": True}],
"order_by": ["-start_in_seconds", "-id"],
},
"system_report_instances_with_tasks_completed_by_me": {
"columns": [
{"Header": "id", "accessor": "id"},
{
"Header": "process_model_display_name",
"accessor": "process_model_display_name",
},
{"Header": "start_in_seconds", "accessor": "start_in_seconds"},
{"Header": "end_in_seconds", "accessor": "end_in_seconds"},
{"Header": "username", "accessor": "username"},
{"Header": "status", "accessor": "status"},
],
"columns": cls.builtin_column_options(),
"filter_by": [
{"field_name": "with_tasks_completed_by_me", "field_value": True}
],
"order_by": ["-start_in_seconds", "-id"],
},
"system_report_instances_with_tasks_completed_by_my_groups": {
"columns": [
{"Header": "id", "accessor": "id"},
{
"Header": "process_model_display_name",
"accessor": "process_model_display_name",
},
{"Header": "start_in_seconds", "accessor": "start_in_seconds"},
{"Header": "end_in_seconds", "accessor": "end_in_seconds"},
{"Header": "username", "accessor": "username"},
{"Header": "status", "accessor": "status"},
],
"columns": cls.builtin_column_options(),
"filter_by": [
{
"field_name": "with_tasks_completed_by_my_group",
"field_value": True,
}
],
"order_by": ["-start_in_seconds", "-id"],
},
}
process_instance_report = ProcessInstanceReportModel(
identifier=report_identifier,
created_by_id=user.id,
report_metadata=temp_system_metadata_map[report_identifier], # type: ignore
report_metadata=temp_system_metadata_map[report_identifier],
)
return process_instance_report # type: ignore
@ -241,3 +228,43 @@ class ProcessInstanceReportService:
)
return report_filter
@classmethod
def add_metadata_columns_to_process_instance(
cls,
process_instance_sqlalchemy_rows: list[sqlalchemy.engine.row.Row], # type: ignore
metadata_columns: list[dict],
) -> list[dict]:
"""Add_metadata_columns_to_process_instance."""
results = []
for process_instance in process_instance_sqlalchemy_rows:
process_instance_dict = process_instance["ProcessInstanceModel"].serialized
for metadata_column in metadata_columns:
if metadata_column["accessor"] not in process_instance_dict:
process_instance_dict[
metadata_column["accessor"]
] = process_instance[metadata_column["accessor"]]
results.append(process_instance_dict)
return results
@classmethod
def get_column_names_for_model(cls, model: db.Model) -> list[str]: # type: ignore
"""Get_column_names_for_model."""
return [i.name for i in model.__table__.columns]
@classmethod
def builtin_column_options(cls) -> list[dict]:
"""Builtin_column_options."""
return [
{"Header": "Id", "accessor": "id", "filterable": False},
{
"Header": "Process",
"accessor": "process_model_display_name",
"filterable": False,
},
{"Header": "Start", "accessor": "start_in_seconds", "filterable": False},
{"Header": "End", "accessor": "end_in_seconds", "filterable": False},
{"Header": "Username", "accessor": "username", "filterable": False},
{"Header": "Status", "accessor": "status", "filterable": False},
]

View File

@ -322,18 +322,3 @@ class ProcessInstanceService:
)
return task
@staticmethod
def serialize_flat_with_task_data(
process_instance: ProcessInstanceModel,
) -> dict[str, Any]:
"""NOTE: This is crazy slow. Put the latest task data in the database."""
"""Serialize_flat_with_task_data."""
# results = {}
# try:
# processor = ProcessInstanceProcessor(process_instance)
# process_instance.data = processor.get_current_data()
# results = process_instance.serialized_flat
# except ApiError:
results = process_instance.serialized
return results

View File

@ -19,7 +19,11 @@
<bpmn:scriptTask id="hot_script_task_OH_YEEEEEEEEEEEEEEEEEEEEAH" name="OHHHHHHHHHHYEEEESSSSSSSSSS">
<bpmn:incoming>Flow_0bazl8x</bpmn:incoming>
<bpmn:outgoing>Flow_1mcaszp</bpmn:outgoing>
<bpmn:script>a = 1</bpmn:script>
<bpmn:script>a = 1
b = 2
outer = {}
outer["inner"] = 'sweet1'
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="Event_1vch1y0">
<bpmn:incoming>Flow_1mcaszp</bpmn:incoming>

View File

@ -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_hk6nsfl" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1ohrjz9</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1ohrjz9" sourceRef="StartEvent_1" targetRef="Activity_0fah9rm" />
<bpmn:endEvent id="Event_1tk4dsv">
<bpmn:incoming>Flow_1flxgry</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_18gs4jt" sourceRef="Activity_0fah9rm" targetRef="Activity_1bvyv67" />
<bpmn:scriptTask id="Activity_0fah9rm" name="First setting of data">
<bpmn:incoming>Flow_1ohrjz9</bpmn:incoming>
<bpmn:outgoing>Flow_18gs4jt</bpmn:outgoing>
<bpmn:script>outer = {}
invoice_number = 123
outer["inner"] = 'sweet1'
outer['time'] = time.time_ns()</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1flxgry" sourceRef="Activity_1bvyv67" targetRef="Event_1tk4dsv" />
<bpmn:scriptTask id="Activity_1bvyv67" name="First setting of data">
<bpmn:incoming>Flow_18gs4jt</bpmn:incoming>
<bpmn:outgoing>Flow_1flxgry</bpmn:outgoing>
<bpmn:script>outer["inner"] = 'sweet2'</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_hk6nsfl">
<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_1c5bi8c_di" bpmnElement="Activity_0fah9rm">
<dc:Bounds x="270" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1tk4dsv_di" bpmnElement="Event_1tk4dsv">
<dc:Bounds x="612" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1ay4o3w_di" bpmnElement="Activity_1bvyv67">
<dc:Bounds x="430" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1ohrjz9_di" bpmnElement="Flow_1ohrjz9">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_18gs4jt_di" bpmnElement="Flow_18gs4jt">
<di:waypoint x="370" y="177" />
<di:waypoint x="430" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1flxgry_di" bpmnElement="Flow_1flxgry">
<di:waypoint x="530" y="177" />
<di:waypoint x="612" y="177" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -265,7 +265,7 @@ class BaseTest:
)
modified_process_model_id = test_process_model_id.replace("/", ":")
response = client.post(
f"/v1.0/process-models/{modified_process_model_id}/process-instances",
f"/v1.0/process-instances/{modified_process_model_id}",
headers=headers,
)
assert response.status_code == 201

View File

@ -57,7 +57,7 @@ class TestLoggingService(BaseTest):
assert response.status_code == 200
log_response = client.get(
f"/v1.0/process-instances/{process_instance_id}/logs",
f"/v1.0/logs/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
headers=headers,
)
assert log_response.status_code == 200

View File

@ -20,6 +20,9 @@ from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.process_group import ProcessGroup
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_instance_metadata import (
ProcessInstanceMetadataModel,
)
from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel,
)
@ -330,6 +333,9 @@ class TestProcessApi(BaseTest):
process_model.display_name = "Updated Display Name"
process_model.primary_file_name = "superduper.bpmn"
process_model.primary_process_id = "superduper"
process_model.metadata_extraction_paths = [
{"key": "extraction1", "path": "path1"}
]
modified_process_model_identifier = process_model_identifier.replace("/", ":")
response = client.put(
@ -343,6 +349,9 @@ class TestProcessApi(BaseTest):
assert response.json["display_name"] == "Updated Display Name"
assert response.json["primary_file_name"] == "superduper.bpmn"
assert response.json["primary_process_id"] == "superduper"
assert response.json["metadata_extraction_paths"] == [
{"key": "extraction1", "path": "path1"}
]
def test_process_model_list_all(
self,
@ -903,7 +912,7 @@ class TestProcessApi(BaseTest):
modified_process_model_identifier = process_model_identifier.replace("/", ":")
response = client.post(
f"/v1.0/process-models/{modified_process_model_identifier}/process-instances",
f"/v1.0/process-instances/{modified_process_model_identifier}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 201
@ -1145,10 +1154,11 @@ class TestProcessApi(BaseTest):
headers=self.logged_in_headers(with_super_admin_user),
)
show_response = client.get(
f"/v1.0/process-models/{modified_process_model_identifier}/process-instances/{process_instance_id}",
f"/v1.0/process-instances/{modified_process_model_identifier}/{process_instance_id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert show_response.json is not None
assert show_response.status_code == 200
file_system_root = FileSystemService.root_path()
file_path = (
f"{file_system_root}/{process_model_identifier}/{process_model_id}.bpmn"
@ -1311,7 +1321,7 @@ class TestProcessApi(BaseTest):
assert response.json is not None
response = client.post(
f"/v1.0/process-instances/{process_instance_id}/terminate",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/terminate",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@ -1358,7 +1368,7 @@ class TestProcessApi(BaseTest):
assert response.json is not None
delete_response = client.delete(
f"/v1.0/process-instances/{process_instance_id}",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert delete_response.status_code == 200
@ -1723,14 +1733,14 @@ class TestProcessApi(BaseTest):
],
}
ProcessInstanceReportModel.create_with_attributes(
report = ProcessInstanceReportModel.create_with_attributes(
identifier="sure",
report_metadata=report_metadata,
user=with_super_admin_user,
)
response = client.get(
"/v1.0/process-instances/reports/sure",
f"/v1.0/process-instances/reports/{report.id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@ -1769,14 +1779,14 @@ class TestProcessApi(BaseTest):
],
}
ProcessInstanceReportModel.create_with_attributes(
report = ProcessInstanceReportModel.create_with_attributes(
identifier="sure",
report_metadata=report_metadata,
user=with_super_admin_user,
)
response = client.get(
"/v1.0/process-instances/reports/sure?grade_level=1",
f"/v1.0/process-instances/reports/{report.id}?grade_level=1",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@ -1791,9 +1801,9 @@ class TestProcessApi(BaseTest):
with_super_admin_user: UserModel,
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_process_instance_report_show_with_default_list."""
"""Test_process_instance_report_show_with_bad_identifier."""
response = client.get(
"/v1.0/process-instances/reports/sure?grade_level=1",
"/v1.0/process-instances/reports/13000000?grade_level=1",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 404
@ -2357,7 +2367,7 @@ class TestProcessApi(BaseTest):
assert process_instance.status == "user_input_required"
client.post(
f"/v1.0/process-instances/{process_instance_id}/suspend",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/suspend",
headers=self.logged_in_headers(with_super_admin_user),
)
process_instance = ProcessInstanceService().get_process_instance(
@ -2661,3 +2671,191 @@ class TestProcessApi(BaseTest):
# )
print("test_process_model_publish")
def test_can_get_process_instance_list_with_report_metadata(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_can_get_process_instance_list_with_report_metadata."""
process_model = load_test_spec(
process_model_id="save_process_instance_metadata/save_process_instance_metadata",
bpmn_file_name="save_process_instance_metadata.bpmn",
process_model_source_directory="save_process_instance_metadata",
)
process_instance = self.create_process_instance_from_process_model(
process_model=process_model, user=with_super_admin_user
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
process_instance_metadata = ProcessInstanceMetadataModel.query.filter_by(
process_instance_id=process_instance.id
).all()
assert len(process_instance_metadata) == 3
report_metadata = {
"columns": [
{"Header": "ID", "accessor": "id"},
{"Header": "Status", "accessor": "status"},
{"Header": "Key One", "accessor": "key1"},
{"Header": "Key Two", "accessor": "key2"},
],
"order_by": ["status"],
"filter_by": [],
}
process_instance_report = ProcessInstanceReportModel.create_with_attributes(
identifier="sure",
report_metadata=report_metadata,
user=with_super_admin_user,
)
response = client.get(
f"/v1.0/process-instances?report_identifier={process_instance_report.identifier}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.json is not None
assert response.status_code == 200
assert len(response.json["results"]) == 1
assert response.json["results"][0]["status"] == "complete"
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["pagination"]["count"] == 1
assert response.json["pagination"]["pages"] == 1
assert response.json["pagination"]["total"] == 1
def test_can_get_process_instance_report_column_list(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_can_get_process_instance_list_with_report_metadata."""
process_model = load_test_spec(
process_model_id="save_process_instance_metadata/save_process_instance_metadata",
bpmn_file_name="save_process_instance_metadata.bpmn",
process_model_source_directory="save_process_instance_metadata",
)
process_instance = self.create_process_instance_from_process_model(
process_model=process_model, user=with_super_admin_user
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
process_instance_metadata = ProcessInstanceMetadataModel.query.filter_by(
process_instance_id=process_instance.id
).all()
assert len(process_instance_metadata) == 3
response = client.get(
"/v1.0/process-instances/reports/columns",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.json is not None
assert response.status_code == 200
assert response.json == [
{"Header": "Id", "accessor": "id", "filterable": False},
{
"Header": "Process",
"accessor": "process_model_display_name",
"filterable": False,
},
{"Header": "Start", "accessor": "start_in_seconds", "filterable": False},
{"Header": "End", "accessor": "end_in_seconds", "filterable": False},
{"Header": "Username", "accessor": "username", "filterable": False},
{"Header": "Status", "accessor": "status", "filterable": False},
{"Header": "key1", "accessor": "key1", "filterable": True},
{"Header": "key2", "accessor": "key2", "filterable": True},
{"Header": "key3", "accessor": "key3", "filterable": True},
]
def test_process_instance_list_can_order_by_metadata(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_process_instance_list_can_order_by_metadata."""
self.create_process_group(
client, with_super_admin_user, "test_group", "test_group"
)
process_model = load_test_spec(
"test_group/hello_world",
process_model_source_directory="nested-task-data-structure",
)
ProcessModelService.update_process_model(
process_model,
{
"metadata_extraction_paths": [
{"key": "time_ns", "path": "outer.time"},
]
},
)
process_instance_one = self.create_process_instance_from_process_model(
process_model
)
processor = ProcessInstanceProcessor(process_instance_one)
processor.do_engine_steps(save=True)
assert process_instance_one.status == "complete"
process_instance_two = self.create_process_instance_from_process_model(
process_model
)
processor = ProcessInstanceProcessor(process_instance_two)
processor.do_engine_steps(save=True)
assert process_instance_two.status == "complete"
report_metadata = {
"columns": [
{"Header": "id", "accessor": "id"},
{"Header": "Time", "accessor": "time_ns"},
],
"order_by": ["time_ns"],
}
report_one = ProcessInstanceReportModel.create_with_attributes(
identifier="report_one",
report_metadata=report_metadata,
user=with_super_admin_user,
)
response = client.get(
f"/v1.0/process-instances?report_id={report_one.id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
assert response.json is not None
assert len(response.json["results"]) == 2
assert response.json["results"][0]["id"] == process_instance_one.id
assert response.json["results"][1]["id"] == process_instance_two.id
report_metadata = {
"columns": [
{"Header": "id", "accessor": "id"},
{"Header": "Time", "accessor": "time_ns"},
],
"order_by": ["-time_ns"],
}
report_two = ProcessInstanceReportModel.create_with_attributes(
identifier="report_two",
report_metadata=report_metadata,
user=with_super_admin_user,
)
response = client.get(
f"/v1.0/process-instances?report_id={report_two.id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
assert response.json is not None
assert len(response.json["results"]) == 2
assert response.json["results"][1]["id"] == process_instance_one.id
assert response.json["results"][0]["id"] == process_instance_two.id

View File

@ -37,7 +37,7 @@ def test_generate_report_with_filter_by_with_variable_substitution(
with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_user_can_be_given_permission_to_administer_process_group."""
"""Test_generate_report_with_filter_by_with_variable_substitution."""
process_instances = setup_process_instances_for_reports
report_metadata = {
"filter_by": [
@ -61,7 +61,7 @@ def test_generate_report_with_order_by_and_one_field(
with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_user_can_be_given_permission_to_administer_process_group."""
"""Test_generate_report_with_order_by_and_one_field."""
process_instances = setup_process_instances_for_reports
report_metadata = {"order_by": ["test_score"]}
results = do_report_with_metadata_and_instances(report_metadata, process_instances)
@ -75,7 +75,7 @@ def test_generate_report_with_order_by_and_two_fields(
with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_user_can_be_given_permission_to_administer_process_group."""
"""Test_generate_report_with_order_by_and_two_fields."""
process_instances = setup_process_instances_for_reports
report_metadata = {"order_by": ["grade_level", "test_score"]}
results = do_report_with_metadata_and_instances(report_metadata, process_instances)
@ -89,7 +89,7 @@ def test_generate_report_with_order_by_desc(
with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_user_can_be_given_permission_to_administer_process_group."""
"""Test_generate_report_with_order_by_desc."""
process_instances = setup_process_instances_for_reports
report_metadata = {"order_by": ["grade_level", "-test_score"]}
results = do_report_with_metadata_and_instances(report_metadata, process_instances)
@ -103,7 +103,7 @@ def test_generate_report_with_columns(
with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_user_can_be_given_permission_to_administer_process_group."""
"""Test_generate_report_with_columns."""
process_instances = setup_process_instances_for_reports
report_metadata = {
"columns": [

View File

@ -5,12 +5,16 @@ from flask_bpmn.models.db import db
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend.models.process_instance_metadata import (
ProcessInstanceMetadataModel,
)
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.process_model_service import ProcessModelService
class TestProcessModel(BaseTest):
@ -122,6 +126,53 @@ class TestProcessModel(BaseTest):
processor.do_engine_steps(save=True)
assert process_instance.status == "complete"
def test_extract_metadata(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_can_run_process_model_with_call_activities."""
self.create_process_group(
client, with_super_admin_user, "test_group", "test_group"
)
process_model = load_test_spec(
"test_group/hello_world",
process_model_source_directory="nested-task-data-structure",
)
ProcessModelService.update_process_model(
process_model,
{
"metadata_extraction_paths": [
{"key": "awesome_var", "path": "outer.inner"},
{"key": "invoice_number", "path": "invoice_number"},
]
},
)
process_instance = self.create_process_instance_from_process_model(
process_model
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
assert process_instance.status == "complete"
process_instance_metadata_awesome_var = (
ProcessInstanceMetadataModel.query.filter_by(
process_instance_id=process_instance.id, key="awesome_var"
).first()
)
assert process_instance_metadata_awesome_var is not None
assert process_instance_metadata_awesome_var.value == "sweet2"
process_instance_metadata_awesome_var = (
ProcessInstanceMetadataModel.query.filter_by(
process_instance_id=process_instance.id, key="invoice_number"
).first()
)
assert process_instance_metadata_awesome_var is not None
assert process_instance_metadata_awesome_var.value == "123"
def create_test_process_model(self, id: str, display_name: str) -> ProcessModelInfo:
"""Create_test_process_model."""
return ProcessModelInfo(