Merge remote-tracking branch 'origin/main' into feature/multiple_editor_users_display_user
This commit is contained in:
commit
f7e8fd0022
|
@ -1289,6 +1289,13 @@ paths:
|
||||||
put:
|
put:
|
||||||
operationId: spiffworkflow_backend.routes.process_models_controller.process_model_file_update
|
operationId: spiffworkflow_backend.routes.process_models_controller.process_model_file_update
|
||||||
summary: save the contents to the given file
|
summary: save the contents to the given file
|
||||||
|
parameters:
|
||||||
|
- name: file_contents_hash
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: The hash of the file contents that originally came with the file.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
tags:
|
tags:
|
||||||
- Process Model Files
|
- Process Model Files
|
||||||
requestBody:
|
requestBody:
|
||||||
|
@ -1632,9 +1639,9 @@ paths:
|
||||||
required: true
|
required: true
|
||||||
description: The unique id of the process instance
|
description: The unique id of the process instance
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: integer
|
||||||
post:
|
post:
|
||||||
operationId: spiffworkflow_backend.routes.process_api_blueprint.send_bpmn_event
|
operationId: spiffworkflow_backend.routes.process_instances_controller.send_bpmn_event
|
||||||
summary: Send a BPMN event to the process
|
summary: Send a BPMN event to the process
|
||||||
tags:
|
tags:
|
||||||
- Process Instances
|
- Process Instances
|
||||||
|
@ -1823,6 +1830,27 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/OkTrue"
|
$ref: "#/components/schemas/OkTrue"
|
||||||
|
|
||||||
|
/tasks/{process_instance_id}/send-user-signal-event:
|
||||||
|
parameters:
|
||||||
|
- name: process_instance_id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The unique id of the process instance
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
post:
|
||||||
|
operationId: spiffworkflow_backend.routes.process_instances_controller.send_user_signal_event
|
||||||
|
summary: Send a BPMN event to the process
|
||||||
|
tags:
|
||||||
|
- Process Instances
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Event Sent Successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Workflow"
|
||||||
|
|
||||||
/messages:
|
/messages:
|
||||||
parameters:
|
parameters:
|
||||||
- name: process_instance_id
|
- name: process_instance_id
|
||||||
|
|
|
@ -127,8 +127,8 @@ class ApiError(Exception):
|
||||||
instance.task_trace = TaskModelError.get_task_trace(task_model)
|
instance.task_trace = TaskModelError.get_task_trace(task_model)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
spec_reference = TaskService.get_spec_reference_from_bpmn_process(task_model.bpmn_process)
|
spec_reference_filename = TaskService.get_spec_filename_from_bpmn_process(task_model.bpmn_process)
|
||||||
instance.file_name = spec_reference.file_name
|
instance.file_name = spec_reference_filename
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
current_app.logger.error(exception)
|
current_app.logger.error(exception)
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
"""File."""
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from dataclasses import field
|
from dataclasses import field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Any
|
||||||
|
|
||||||
import marshmallow
|
|
||||||
from marshmallow import INCLUDE
|
|
||||||
from marshmallow import Schema
|
|
||||||
|
|
||||||
from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum
|
from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum
|
||||||
from spiffworkflow_backend.models.spec_reference import SpecReference
|
from spiffworkflow_backend.models.spec_reference import SpecReference
|
||||||
|
@ -74,10 +71,10 @@ class File:
|
||||||
type: str
|
type: str
|
||||||
last_modified: datetime
|
last_modified: datetime
|
||||||
size: int
|
size: int
|
||||||
references: Optional[list[SpecReference]] = None
|
references: list[SpecReference] | None = None
|
||||||
file_contents: Optional[bytes] = None
|
file_contents: bytes | None = None
|
||||||
process_model_id: Optional[str] = None
|
process_model_id: str | None = None
|
||||||
process_group_id: Optional[str] = None
|
file_contents_hash: str | None = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
"""__post_init__."""
|
"""__post_init__."""
|
||||||
|
@ -91,7 +88,7 @@ class File:
|
||||||
content_type: str,
|
content_type: str,
|
||||||
last_modified: datetime,
|
last_modified: datetime,
|
||||||
file_size: int,
|
file_size: int,
|
||||||
) -> "File":
|
) -> File:
|
||||||
"""From_file_system."""
|
"""From_file_system."""
|
||||||
instance = cls(
|
instance = cls(
|
||||||
name=file_name,
|
name=file_name,
|
||||||
|
@ -102,28 +99,9 @@ class File:
|
||||||
)
|
)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
@property
|
||||||
class FileSchema(Schema):
|
def serialized(self) -> dict[str, Any]:
|
||||||
"""FileSchema."""
|
dictionary = self.__dict__
|
||||||
|
if isinstance(self.file_contents, bytes):
|
||||||
class Meta:
|
dictionary["file_contents"] = self.file_contents.decode("utf-8")
|
||||||
"""Meta."""
|
return dictionary
|
||||||
|
|
||||||
model = File
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"content_type",
|
|
||||||
"last_modified",
|
|
||||||
"type",
|
|
||||||
"size",
|
|
||||||
"data_store",
|
|
||||||
"user_uid",
|
|
||||||
"url",
|
|
||||||
"file_contents",
|
|
||||||
"references",
|
|
||||||
"process_group_id",
|
|
||||||
"process_model_id",
|
|
||||||
]
|
|
||||||
unknown = INCLUDE
|
|
||||||
references = marshmallow.fields.List(marshmallow.fields.Nested("SpecReferenceSchema"))
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ class ProcessModelInfoSchema(Schema):
|
||||||
display_order = marshmallow.fields.Integer(allow_none=True)
|
display_order = marshmallow.fields.Integer(allow_none=True)
|
||||||
primary_file_name = marshmallow.fields.String(allow_none=True)
|
primary_file_name = marshmallow.fields.String(allow_none=True)
|
||||||
primary_process_id = marshmallow.fields.String(allow_none=True)
|
primary_process_id = marshmallow.fields.String(allow_none=True)
|
||||||
files = marshmallow.fields.List(marshmallow.fields.Nested("FileSchema"))
|
files = marshmallow.fields.List(marshmallow.fields.Nested("File"))
|
||||||
fault_or_suspend_on_exception = marshmallow.fields.String()
|
fault_or_suspend_on_exception = marshmallow.fields.String()
|
||||||
exception_notification_addresses = marshmallow.fields.List(marshmallow.fields.String)
|
exception_notification_addresses = marshmallow.fields.List(marshmallow.fields.String)
|
||||||
metadata_extraction_paths = marshmallow.fields.List(
|
metadata_extraction_paths = marshmallow.fields.List(
|
||||||
|
|
|
@ -31,7 +31,6 @@ from spiffworkflow_backend.services.process_caller_service import ProcessCallerS
|
||||||
from spiffworkflow_backend.services.process_instance_processor import (
|
from spiffworkflow_backend.services.process_instance_processor import (
|
||||||
ProcessInstanceProcessor,
|
ProcessInstanceProcessor,
|
||||||
)
|
)
|
||||||
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
|
||||||
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
||||||
|
|
||||||
|
|
||||||
|
@ -189,25 +188,6 @@ def _get_required_parameter_or_raise(parameter: str, post_body: dict[str, Any])
|
||||||
return return_value
|
return return_value
|
||||||
|
|
||||||
|
|
||||||
def send_bpmn_event(
|
|
||||||
modified_process_model_identifier: str,
|
|
||||||
process_instance_id: str,
|
|
||||||
body: Dict,
|
|
||||||
) -> Response:
|
|
||||||
"""Send a bpmn event to a workflow."""
|
|
||||||
process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first()
|
|
||||||
if process_instance:
|
|
||||||
processor = ProcessInstanceProcessor(process_instance)
|
|
||||||
processor.send_bpmn_event(body)
|
|
||||||
task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
|
|
||||||
return make_response(jsonify(task), 200)
|
|
||||||
else:
|
|
||||||
raise ApiError(
|
|
||||||
error_code="send_bpmn_event_error",
|
|
||||||
message=f"Could not send event to Instance: {process_instance_id}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _commit_and_push_to_git(message: str) -> None:
|
def _commit_and_push_to_git(message: str) -> None:
|
||||||
"""Commit_and_push_to_git."""
|
"""Commit_and_push_to_git."""
|
||||||
if current_app.config["SPIFFWORKFLOW_BACKEND_GIT_COMMIT_ON_SAVE"]:
|
if current_app.config["SPIFFWORKFLOW_BACKEND_GIT_COMMIT_ON_SAVE"]:
|
||||||
|
|
|
@ -298,8 +298,8 @@ def process_instance_report_show(
|
||||||
def process_instance_report_column_list(
|
def process_instance_report_column_list(
|
||||||
process_model_identifier: Optional[str] = None,
|
process_model_identifier: Optional[str] = None,
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
"""Process_instance_report_column_list."""
|
|
||||||
table_columns = ProcessInstanceReportService.builtin_column_options()
|
table_columns = ProcessInstanceReportService.builtin_column_options()
|
||||||
|
system_report_column_options = ProcessInstanceReportService.system_report_column_options()
|
||||||
columns_for_metadata_query = (
|
columns_for_metadata_query = (
|
||||||
db.session.query(ProcessInstanceMetadataModel.key)
|
db.session.query(ProcessInstanceMetadataModel.key)
|
||||||
.order_by(ProcessInstanceMetadataModel.key)
|
.order_by(ProcessInstanceMetadataModel.key)
|
||||||
|
@ -315,7 +315,7 @@ def process_instance_report_column_list(
|
||||||
columns_for_metadata_strings = [
|
columns_for_metadata_strings = [
|
||||||
{"Header": i[0], "accessor": i[0], "filterable": True} for i in columns_for_metadata
|
{"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)
|
return make_response(jsonify(table_columns + system_report_column_options + columns_for_metadata_strings), 200)
|
||||||
|
|
||||||
|
|
||||||
def process_instance_show_for_me(
|
def process_instance_show_for_me(
|
||||||
|
@ -642,6 +642,38 @@ def process_instance_find_by_id(
|
||||||
return make_response(jsonify(response_json), 200)
|
return make_response(jsonify(response_json), 200)
|
||||||
|
|
||||||
|
|
||||||
|
def send_user_signal_event(
|
||||||
|
process_instance_id: int,
|
||||||
|
body: Dict,
|
||||||
|
) -> Response:
|
||||||
|
"""Send a user signal event to a process instance."""
|
||||||
|
process_instance = _find_process_instance_for_me_or_raise(process_instance_id)
|
||||||
|
return _send_bpmn_event(process_instance, body)
|
||||||
|
|
||||||
|
|
||||||
|
def send_bpmn_event(
|
||||||
|
modified_process_model_identifier: str,
|
||||||
|
process_instance_id: int,
|
||||||
|
body: Dict,
|
||||||
|
) -> Response:
|
||||||
|
"""Send a bpmn event to a process instance."""
|
||||||
|
process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first()
|
||||||
|
if process_instance:
|
||||||
|
return _send_bpmn_event(process_instance, body)
|
||||||
|
else:
|
||||||
|
raise ApiError(
|
||||||
|
error_code="send_bpmn_event_error",
|
||||||
|
message=f"Could not send event to Instance: {process_instance_id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_bpmn_event(process_instance: ProcessInstanceModel, body: dict) -> Response:
|
||||||
|
processor = ProcessInstanceProcessor(process_instance)
|
||||||
|
processor.send_bpmn_event(body)
|
||||||
|
task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
|
||||||
|
return make_response(jsonify(task), 200)
|
||||||
|
|
||||||
|
|
||||||
def _get_process_instance(
|
def _get_process_instance(
|
||||||
modified_process_model_identifier: str,
|
modified_process_model_identifier: str,
|
||||||
process_instance: ProcessInstanceModel,
|
process_instance: ProcessInstanceModel,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from hashlib import sha256
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
@ -18,7 +19,6 @@ from werkzeug.datastructures import FileStorage
|
||||||
|
|
||||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||||
from spiffworkflow_backend.interfaces import IdToProcessGroupMapping
|
from spiffworkflow_backend.interfaces import IdToProcessGroupMapping
|
||||||
from spiffworkflow_backend.models.file import FileSchema
|
|
||||||
from spiffworkflow_backend.models.process_group import ProcessGroup
|
from spiffworkflow_backend.models.process_group import ProcessGroup
|
||||||
from spiffworkflow_backend.models.process_instance_report import (
|
from spiffworkflow_backend.models.process_instance_report import (
|
||||||
ProcessInstanceReportModel,
|
ProcessInstanceReportModel,
|
||||||
|
@ -245,10 +245,13 @@ def process_model_list(
|
||||||
return make_response(jsonify(response_json), 200)
|
return make_response(jsonify(response_json), 200)
|
||||||
|
|
||||||
|
|
||||||
def process_model_file_update(modified_process_model_identifier: str, file_name: str) -> flask.wrappers.Response:
|
def process_model_file_update(
|
||||||
"""Process_model_file_update."""
|
modified_process_model_identifier: str, file_name: str, file_contents_hash: str
|
||||||
|
) -> flask.wrappers.Response:
|
||||||
message = f"User: {g.user.username} clicked save for"
|
message = f"User: {g.user.username} clicked save for"
|
||||||
return _create_or_update_process_model_file(modified_process_model_identifier, message, 200)
|
return _create_or_update_process_model_file(
|
||||||
|
modified_process_model_identifier, message, 200, file_contents_hash=file_contents_hash
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def process_model_file_delete(modified_process_model_identifier: str, file_name: str) -> flask.wrappers.Response:
|
def process_model_file_delete(modified_process_model_identifier: str, file_name: str) -> flask.wrappers.Response:
|
||||||
|
@ -293,24 +296,23 @@ def process_model_file_create(
|
||||||
|
|
||||||
|
|
||||||
def process_model_file_show(modified_process_model_identifier: str, file_name: str) -> Any:
|
def process_model_file_show(modified_process_model_identifier: str, file_name: str) -> Any:
|
||||||
"""Process_model_file_show."""
|
|
||||||
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
||||||
process_model = _get_process_model(process_model_identifier)
|
process_model = _get_process_model(process_model_identifier)
|
||||||
files = SpecFileService.get_files(process_model, file_name)
|
files = SpecFileService.get_files(process_model, file_name)
|
||||||
if len(files) == 0:
|
if len(files) == 0:
|
||||||
raise ApiError(
|
raise ApiError(
|
||||||
error_code="unknown file",
|
error_code="process_model_file_not_found",
|
||||||
message=(
|
message=f"File {file_name} not found in workflow {process_model_identifier}.",
|
||||||
f"No information exists for file {file_name} it does not exist in workflow {process_model_identifier}."
|
|
||||||
),
|
|
||||||
status_code=404,
|
status_code=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
file = files[0]
|
file = files[0]
|
||||||
file_contents = SpecFileService.get_data(process_model, file.name)
|
file_contents = SpecFileService.get_data(process_model, file.name)
|
||||||
file.file_contents = file_contents
|
file.file_contents = file_contents
|
||||||
|
file_contents_hash = sha256(file_contents).hexdigest()
|
||||||
|
file.file_contents_hash = file_contents_hash
|
||||||
file.process_model_id = process_model.id
|
file.process_model_id = process_model.id
|
||||||
return FileSchema().dump(file)
|
return make_response(jsonify(file), 200)
|
||||||
|
|
||||||
|
|
||||||
# {
|
# {
|
||||||
|
@ -477,6 +479,7 @@ def _create_or_update_process_model_file(
|
||||||
modified_process_model_identifier: str,
|
modified_process_model_identifier: str,
|
||||||
message_for_git_commit: str,
|
message_for_git_commit: str,
|
||||||
http_status_to_return: int,
|
http_status_to_return: int,
|
||||||
|
file_contents_hash: Optional[str] = None,
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
"""_create_or_update_process_model_file."""
|
"""_create_or_update_process_model_file."""
|
||||||
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
||||||
|
@ -498,6 +501,21 @@ def _create_or_update_process_model_file(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if file_contents_hash is not None:
|
||||||
|
current_file_contents_bytes = SpecFileService.get_data(process_model, request_file.filename)
|
||||||
|
if current_file_contents_bytes and file_contents_hash:
|
||||||
|
current_file_contents_hash = sha256(current_file_contents_bytes).hexdigest()
|
||||||
|
if current_file_contents_hash != file_contents_hash:
|
||||||
|
raise ApiError(
|
||||||
|
error_code="process_model_file_has_changed",
|
||||||
|
message=(
|
||||||
|
f"Process model file: {request_file.filename} was already changed by someone else. If you made"
|
||||||
|
" changes you do not want to lose, click the Download button and make sure your changes are"
|
||||||
|
" in the resulting file. If you do not need your changes, you can safely reload this page."
|
||||||
|
),
|
||||||
|
status_code=409,
|
||||||
|
)
|
||||||
|
|
||||||
file = None
|
file = None
|
||||||
try:
|
try:
|
||||||
file = SpecFileService.update_file(process_model, request_file.filename, request_file_contents)
|
file = SpecFileService.update_file(process_model, request_file.filename, request_file_contents)
|
||||||
|
@ -514,8 +532,4 @@ def _create_or_update_process_model_file(
|
||||||
file.process_model_id = process_model.id
|
file.process_model_id = process_model.id
|
||||||
_commit_and_push_to_git(f"{message_for_git_commit} {process_model_identifier}/{file.name}")
|
_commit_and_push_to_git(f"{message_for_git_commit} {process_model_identifier}/{file.name}")
|
||||||
|
|
||||||
return Response(
|
return make_response(jsonify(file), http_status_to_return)
|
||||||
json.dumps(FileSchema().dump(file)),
|
|
||||||
status=http_status_to_return,
|
|
||||||
mimetype="application/json",
|
|
||||||
)
|
|
||||||
|
|
|
@ -532,8 +532,10 @@ class AuthorizationService:
|
||||||
# 1. view your own instances.
|
# 1. view your own instances.
|
||||||
# 2. view the logs for these instances.
|
# 2. view the logs for these instances.
|
||||||
if permission_set == "start":
|
if permission_set == "start":
|
||||||
target_uri = f"/process-instances/{process_related_path_segment}"
|
path_prefixes_that_allow_create_access = ["process-instances"]
|
||||||
permissions_to_assign.append(PermissionToAssign(permission="create", target_uri=target_uri))
|
for path_prefix in path_prefixes_that_allow_create_access:
|
||||||
|
target_uri = f"/{path_prefix}/{process_related_path_segment}"
|
||||||
|
permissions_to_assign.append(PermissionToAssign(permission="create", target_uri=target_uri))
|
||||||
|
|
||||||
# giving people access to all logs for an instance actually gives them a little bit more access
|
# giving people access to all logs for an instance actually gives them a little bit more access
|
||||||
# than would be optimal. ideally, you would only be able to view the logs for instances that you started
|
# than would be optimal. ideally, you would only be able to view the logs for instances that you started
|
||||||
|
|
|
@ -1115,59 +1115,54 @@ class ProcessInstanceProcessor:
|
||||||
|
|
||||||
def manual_complete_task(self, task_id: str, execute: bool) -> None:
|
def manual_complete_task(self, task_id: str, execute: bool) -> None:
|
||||||
"""Mark the task complete optionally executing it."""
|
"""Mark the task complete optionally executing it."""
|
||||||
spiff_tasks_updated = {}
|
|
||||||
start_in_seconds = time.time()
|
start_in_seconds = time.time()
|
||||||
spiff_task = self.bpmn_process_instance.get_task_from_id(UUID(task_id))
|
spiff_task = self.bpmn_process_instance.get_task_from_id(UUID(task_id))
|
||||||
event_type = ProcessInstanceEventType.task_skipped.value
|
event_type = ProcessInstanceEventType.task_skipped.value
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
if execute:
|
if execute:
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"Manually executing Task {spiff_task.task_spec.name} of process"
|
f"Manually executing Task {spiff_task.task_spec.name} of process"
|
||||||
f" instance {self.process_instance_model.id}"
|
f" instance {self.process_instance_model.id}"
|
||||||
)
|
)
|
||||||
# Executing a subworkflow manually will restart its subprocess and allow stepping through it
|
# Executing a sub-workflow manually will restart its subprocess and allow stepping through it
|
||||||
if isinstance(spiff_task.task_spec, SubWorkflowTask):
|
if isinstance(spiff_task.task_spec, SubWorkflowTask):
|
||||||
subprocess = self.bpmn_process_instance.get_subprocess(spiff_task)
|
subprocess = self.bpmn_process_instance.get_subprocess(spiff_task)
|
||||||
# We have to get to the actual start event
|
# We have to get to the actual start event
|
||||||
for task in self.bpmn_process_instance.get_tasks(workflow=subprocess):
|
for spiff_task in self.bpmn_process_instance.get_tasks(workflow=subprocess):
|
||||||
task.complete()
|
spiff_task.run()
|
||||||
spiff_tasks_updated[task.id] = task
|
if isinstance(spiff_task.task_spec, StartEvent):
|
||||||
if isinstance(task.task_spec, StartEvent):
|
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
spiff_task.complete()
|
spiff_task.run()
|
||||||
spiff_tasks_updated[spiff_task.id] = spiff_task
|
|
||||||
for child in spiff_task.children:
|
|
||||||
spiff_tasks_updated[child.id] = child
|
|
||||||
event_type = ProcessInstanceEventType.task_executed_manually.value
|
event_type = ProcessInstanceEventType.task_executed_manually.value
|
||||||
else:
|
else:
|
||||||
spiff_logger = logging.getLogger("spiff")
|
spiff_logger = logging.getLogger("spiff")
|
||||||
spiff_logger.info(f"Skipped task {spiff_task.task_spec.name}", extra=spiff_task.log_info())
|
spiff_logger.info(f"Skipped task {spiff_task.task_spec.name}", extra=spiff_task.log_info())
|
||||||
spiff_task._set_state(TaskState.COMPLETED)
|
spiff_task.complete()
|
||||||
for child in spiff_task.children:
|
|
||||||
child.task_spec._update(child)
|
|
||||||
spiff_tasks_updated[child.id] = child
|
|
||||||
spiff_task.workflow.last_task = spiff_task
|
spiff_task.workflow.last_task = spiff_task
|
||||||
spiff_tasks_updated[spiff_task.id] = spiff_task
|
|
||||||
|
|
||||||
end_in_seconds = time.time()
|
end_in_seconds = time.time()
|
||||||
|
|
||||||
if isinstance(spiff_task.task_spec, EndEvent):
|
if isinstance(spiff_task.task_spec, EndEvent):
|
||||||
for task in self.bpmn_process_instance.get_tasks(TaskState.DEFINITE_MASK, workflow=spiff_task.workflow):
|
for task in self.bpmn_process_instance.get_tasks(TaskState.DEFINITE_MASK, workflow=spiff_task.workflow):
|
||||||
task.complete()
|
task.complete()
|
||||||
spiff_tasks_updated[task.id] = task
|
|
||||||
|
|
||||||
# A subworkflow task will become ready when its workflow is complete. Engine steps would normally
|
# A subworkflow task will become ready when its workflow is complete. Engine steps would normally
|
||||||
# then complete it, but we have to do it ourselves here.
|
# then complete it, but we have to do it ourselves here.
|
||||||
for task in self.bpmn_process_instance.get_tasks(TaskState.READY):
|
for task in self.bpmn_process_instance.get_tasks(TaskState.READY):
|
||||||
if isinstance(task.task_spec, SubWorkflowTask):
|
if isinstance(task.task_spec, SubWorkflowTask):
|
||||||
task.complete()
|
task.complete()
|
||||||
spiff_tasks_updated[task.id] = task
|
|
||||||
|
|
||||||
task_service = TaskService(
|
task_service = TaskService(
|
||||||
process_instance=self.process_instance_model,
|
process_instance=self.process_instance_model,
|
||||||
serializer=self._serializer,
|
serializer=self._serializer,
|
||||||
bpmn_definition_to_task_definitions_mappings=self.bpmn_definition_to_task_definitions_mappings,
|
bpmn_definition_to_task_definitions_mappings=self.bpmn_definition_to_task_definitions_mappings,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
spiff_tasks_updated = {}
|
||||||
|
for task in self.bpmn_process_instance.get_tasks():
|
||||||
|
if task.last_state_change > start_time:
|
||||||
|
spiff_tasks_updated[task.id] = task
|
||||||
for updated_spiff_task in spiff_tasks_updated.values():
|
for updated_spiff_task in spiff_tasks_updated.values():
|
||||||
(
|
(
|
||||||
bpmn_process,
|
bpmn_process,
|
||||||
|
@ -1216,6 +1211,14 @@ class ProcessInstanceProcessor:
|
||||||
raise TaskNotFoundError(
|
raise TaskNotFoundError(
|
||||||
f"Cannot find a task with guid '{to_task_guid}' for process instance '{process_instance.id}'"
|
f"Cannot find a task with guid '{to_task_guid}' for process instance '{process_instance.id}'"
|
||||||
)
|
)
|
||||||
|
# If this task model has a parent boundary event, reset to that point instead,
|
||||||
|
# so we can reset all the boundary timers, etc...
|
||||||
|
parent_id = to_task_model.properties_json.get("parent", "")
|
||||||
|
parent = TaskModel.query.filter_by(guid=parent_id).first()
|
||||||
|
is_boundary_parent = False
|
||||||
|
if parent and parent.task_definition.typename == "_BoundaryEventParent":
|
||||||
|
to_task_model = parent
|
||||||
|
is_boundary_parent = True # Will need to complete this task at the end so we are on the correct process.
|
||||||
|
|
||||||
# NOTE: run ALL queries before making changes to ensure we get everything before anything changes
|
# NOTE: run ALL queries before making changes to ensure we get everything before anything changes
|
||||||
parent_bpmn_processes, task_models_of_parent_bpmn_processes = TaskService.task_models_of_parent_bpmn_processes(
|
parent_bpmn_processes, task_models_of_parent_bpmn_processes = TaskService.task_models_of_parent_bpmn_processes(
|
||||||
|
@ -1320,6 +1323,11 @@ class ProcessInstanceProcessor:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
processor = ProcessInstanceProcessor(process_instance)
|
processor = ProcessInstanceProcessor(process_instance)
|
||||||
|
|
||||||
|
# If this as a boundary event parent, run it, so we get back to an active task.
|
||||||
|
if is_boundary_parent:
|
||||||
|
processor.do_engine_steps(execution_strategy_name="one_at_a_time")
|
||||||
|
|
||||||
processor.save()
|
processor.save()
|
||||||
processor.suspend()
|
processor.suspend()
|
||||||
|
|
||||||
|
|
|
@ -320,7 +320,7 @@ class ProcessInstanceReportService:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def builtin_column_options(cls) -> list[ReportMetadataColumn]:
|
def builtin_column_options(cls) -> list[ReportMetadataColumn]:
|
||||||
"""Builtin_column_options."""
|
"""Columns that are actually in the process instance table."""
|
||||||
return_value: list[ReportMetadataColumn] = [
|
return_value: list[ReportMetadataColumn] = [
|
||||||
{"Header": "Id", "accessor": "id", "filterable": False},
|
{"Header": "Id", "accessor": "id", "filterable": False},
|
||||||
{
|
{
|
||||||
|
@ -339,6 +339,15 @@ class ProcessInstanceReportService:
|
||||||
]
|
]
|
||||||
return return_value
|
return return_value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def system_report_column_options(cls) -> list[ReportMetadataColumn]:
|
||||||
|
"""Columns that are used with certain system reports."""
|
||||||
|
return_value: list[ReportMetadataColumn] = [
|
||||||
|
{"Header": "Task", "accessor": "task_title", "filterable": False},
|
||||||
|
{"Header": "Waiting For", "accessor": "waiting_for", "filterable": False},
|
||||||
|
]
|
||||||
|
return return_value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_filter_value(cls, filters: list[FilterValue], filter_key: str) -> Any:
|
def get_filter_value(cls, filters: list[FilterValue], filter_key: str) -> Any:
|
||||||
for filter in filters:
|
for filter in filters:
|
||||||
|
|
|
@ -175,7 +175,6 @@ class SpecFileService(FileSystemService):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_file(cls, process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes) -> File:
|
def update_file(cls, process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes) -> File:
|
||||||
"""Update_file."""
|
|
||||||
SpecFileService.assert_valid_file_name(file_name)
|
SpecFileService.assert_valid_file_name(file_name)
|
||||||
cls.validate_bpmn_xml(file_name, binary_data)
|
cls.validate_bpmn_xml(file_name, binary_data)
|
||||||
|
|
||||||
|
|
|
@ -89,15 +89,15 @@ class TaskModelError(Exception):
|
||||||
task_definition = task_model.task_definition
|
task_definition = task_model.task_definition
|
||||||
task_bpmn_name = TaskService.get_name_for_display(task_definition)
|
task_bpmn_name = TaskService.get_name_for_display(task_definition)
|
||||||
bpmn_process = task_model.bpmn_process
|
bpmn_process = task_model.bpmn_process
|
||||||
spec_reference = TaskService.get_spec_reference_from_bpmn_process(bpmn_process)
|
spec_reference_filename = TaskService.get_spec_filename_from_bpmn_process(bpmn_process)
|
||||||
|
|
||||||
task_trace = [f"{task_bpmn_name} ({spec_reference.file_name})"]
|
task_trace = [f"{task_bpmn_name} ({spec_reference_filename})"]
|
||||||
while bpmn_process.guid is not None:
|
while bpmn_process.guid is not None:
|
||||||
caller_task_model = TaskModel.query.filter_by(guid=bpmn_process.guid).first()
|
caller_task_model = TaskModel.query.filter_by(guid=bpmn_process.guid).first()
|
||||||
bpmn_process = BpmnProcessModel.query.filter_by(id=bpmn_process.direct_parent_process_id).first()
|
bpmn_process = BpmnProcessModel.query.filter_by(id=bpmn_process.direct_parent_process_id).first()
|
||||||
spec_reference = TaskService.get_spec_reference_from_bpmn_process(bpmn_process)
|
spec_reference_filename = TaskService.get_spec_filename_from_bpmn_process(bpmn_process)
|
||||||
task_trace.append(
|
task_trace.append(
|
||||||
f"{TaskService.get_name_for_display(caller_task_model.task_definition)} ({spec_reference.file_name})"
|
f"{TaskService.get_name_for_display(caller_task_model.task_definition)} ({spec_reference_filename})"
|
||||||
)
|
)
|
||||||
return task_trace
|
return task_trace
|
||||||
|
|
||||||
|
@ -630,6 +630,15 @@ class TaskService:
|
||||||
result.append({"event": event_definition, "label": extensions["signalButtonLabel"]})
|
result.append({"event": event_definition, "label": extensions["signalButtonLabel"]})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_spec_filename_from_bpmn_process(cls, bpmn_process: BpmnProcessModel) -> Optional[str]:
|
||||||
|
"""Just return the filename if the bpmn process is found in spec reference cache."""
|
||||||
|
try:
|
||||||
|
filename: Optional[str] = cls.get_spec_reference_from_bpmn_process(bpmn_process).file_name
|
||||||
|
return filename
|
||||||
|
except SpecReferenceNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_spec_reference_from_bpmn_process(cls, bpmn_process: BpmnProcessModel) -> SpecReferenceCache:
|
def get_spec_reference_from_bpmn_process(cls, bpmn_process: BpmnProcessModel) -> SpecReferenceCache:
|
||||||
"""Get the bpmn file for a given task model.
|
"""Get the bpmn file for a given task model.
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?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" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||||
|
<bpmn:process id="Process_Admin_Tools_Test" name="AdminToolsTest" isExecutable="true">
|
||||||
|
<bpmn:startEvent id="Event_17e2qgy">
|
||||||
|
<bpmn:outgoing>Flow_1ist4rn</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1ist4rn" sourceRef="Event_17e2qgy" targetRef="Activity_039a4i7" />
|
||||||
|
<bpmn:endEvent id="Event_1qodpuj">
|
||||||
|
<bpmn:incoming>Flow_1xbry1g</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_0vzi07z" sourceRef="Activity_039a4i7" targetRef="Activity_0sqxs4d" />
|
||||||
|
<bpmn:callActivity id="Activity_039a4i7" calledElement="Process_With_Timer">
|
||||||
|
<bpmn:incoming>Flow_1ist4rn</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_0vzi07z</bpmn:outgoing>
|
||||||
|
</bpmn:callActivity>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1xbry1g" sourceRef="Activity_0sqxs4d" targetRef="Event_1qodpuj" />
|
||||||
|
<bpmn:manualTask id="Activity_0sqxs4d" name="Final">
|
||||||
|
<bpmn:incoming>Flow_0vzi07z</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_1xbry1g</bpmn:outgoing>
|
||||||
|
</bpmn:manualTask>
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_Admin_Tools_Test">
|
||||||
|
<bpmndi:BPMNShape id="Event_17e2qgy_di" bpmnElement="Event_17e2qgy">
|
||||||
|
<dc:Bounds x="352" y="152" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_02srymo_di" bpmnElement="Activity_039a4i7">
|
||||||
|
<dc:Bounds x="440" y="130" width="100" height="80" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_1qodpuj_di" bpmnElement="Event_1qodpuj">
|
||||||
|
<dc:Bounds x="742" y="152" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_1r3vbnd_di" bpmnElement="Activity_0sqxs4d">
|
||||||
|
<dc:Bounds x="600" y="130" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1ist4rn_di" bpmnElement="Flow_1ist4rn">
|
||||||
|
<di:waypoint x="388" y="170" />
|
||||||
|
<di:waypoint x="440" y="170" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0vzi07z_di" bpmnElement="Flow_0vzi07z">
|
||||||
|
<di:waypoint x="540" y="170" />
|
||||||
|
<di:waypoint x="600" y="170" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1xbry1g_di" bpmnElement="Flow_1xbry1g">
|
||||||
|
<di:waypoint x="700" y="170" />
|
||||||
|
<di:waypoint x="742" y="170" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" 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_With_Timer" name="Process With Timer" isExecutable="true">
|
||||||
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
|
<bpmn:outgoing>Flow_1e5apvr</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1e5apvr" sourceRef="StartEvent_1" targetRef="manual_task_1" />
|
||||||
|
<bpmn:sequenceFlow id="Flow_0vtgres" sourceRef="manual_task_1" targetRef="Activity_2" />
|
||||||
|
<bpmn:endEvent id="Event_1pgaya7">
|
||||||
|
<bpmn:incoming>Flow_110vf76</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_110vf76" sourceRef="Activity_2" targetRef="Event_1pgaya7" />
|
||||||
|
<bpmn:boundaryEvent id="Timer_Event_Horror" name="Timer_Event_Horror" attachedToRef="manual_task_1">
|
||||||
|
<bpmn:outgoing>Flow_1hy0t7d</bpmn:outgoing>
|
||||||
|
<bpmn:timerEventDefinition id="TimerEventDefinition_1jkwn61">
|
||||||
|
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">'P14D'</bpmn:timeDuration>
|
||||||
|
</bpmn:timerEventDefinition>
|
||||||
|
</bpmn:boundaryEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1hy0t7d" sourceRef="Timer_Event_Horror" targetRef="Activity_3" />
|
||||||
|
<bpmn:endEvent id="Event_10frcbe">
|
||||||
|
<bpmn:incoming>Flow_1xbdri7</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1xbdri7" sourceRef="Activity_3" targetRef="Event_10frcbe" />
|
||||||
|
<bpmn:manualTask id="manual_task_1" name="Manual Task #1">
|
||||||
|
<bpmn:incoming>Flow_1e5apvr</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_0vtgres</bpmn:outgoing>
|
||||||
|
</bpmn:manualTask>
|
||||||
|
<bpmn:manualTask id="Activity_3" name="#3">
|
||||||
|
<bpmn:incoming>Flow_1hy0t7d</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_1xbdri7</bpmn:outgoing>
|
||||||
|
</bpmn:manualTask>
|
||||||
|
<bpmn:scriptTask id="Activity_2" name="#2">
|
||||||
|
<bpmn:incoming>Flow_0vtgres</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_110vf76</bpmn:outgoing>
|
||||||
|
<bpmn:script>y='1000'</bpmn:script>
|
||||||
|
</bpmn:scriptTask>
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_With_Timer">
|
||||||
|
<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_1pgaya7_di" bpmnElement="Event_1pgaya7">
|
||||||
|
<dc:Bounds x="592" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_10frcbe_di" bpmnElement="Event_10frcbe">
|
||||||
|
<dc:Bounds x="592" y="282" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_1cl74h9_di" bpmnElement="manual_task_1">
|
||||||
|
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0yn987b_di" bpmnElement="Activity_3">
|
||||||
|
<dc:Bounds x="430" y="260" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_19c5vp3_di" bpmnElement="Activity_2">
|
||||||
|
<dc:Bounds x="430" y="137" width="100" height="80" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_1gfn4de_di" bpmnElement="Timer_Event_Horror">
|
||||||
|
<dc:Bounds x="302" y="199" width="36" height="36" />
|
||||||
|
<bpmndi:BPMNLabel>
|
||||||
|
<dc:Bounds x="277" y="242" width="87" height="27" />
|
||||||
|
</bpmndi:BPMNLabel>
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1e5apvr_di" bpmnElement="Flow_1e5apvr">
|
||||||
|
<di:waypoint x="215" y="177" />
|
||||||
|
<di:waypoint x="270" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0vtgres_di" bpmnElement="Flow_0vtgres">
|
||||||
|
<di:waypoint x="370" y="177" />
|
||||||
|
<di:waypoint x="430" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_110vf76_di" bpmnElement="Flow_110vf76">
|
||||||
|
<di:waypoint x="530" y="177" />
|
||||||
|
<di:waypoint x="592" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1hy0t7d_di" bpmnElement="Flow_1hy0t7d">
|
||||||
|
<di:waypoint x="320" y="235" />
|
||||||
|
<di:waypoint x="320" y="300" />
|
||||||
|
<di:waypoint x="430" y="300" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1xbdri7_di" bpmnElement="Flow_1xbdri7">
|
||||||
|
<di:waypoint x="530" y="300" />
|
||||||
|
<di:waypoint x="592" y="300" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
|
@ -113,7 +113,6 @@ class BaseTest:
|
||||||
process_group_id: str,
|
process_group_id: str,
|
||||||
display_name: str = "",
|
display_name: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create_process_group."""
|
|
||||||
process_group = ProcessGroup(id=process_group_id, display_name=display_name, display_order=0, admin=False)
|
process_group = ProcessGroup(id=process_group_id, display_name=display_name, display_order=0, admin=False)
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/v1.0/process-groups",
|
"/v1.0/process-groups",
|
||||||
|
@ -138,7 +137,6 @@ class BaseTest:
|
||||||
primary_file_name: Optional[str] = None,
|
primary_file_name: Optional[str] = None,
|
||||||
user: Optional[UserModel] = None,
|
user: Optional[UserModel] = None,
|
||||||
) -> TestResponse:
|
) -> TestResponse:
|
||||||
"""Create_process_model."""
|
|
||||||
if process_model_id is not None:
|
if process_model_id is not None:
|
||||||
# make sure we have a group
|
# make sure we have a group
|
||||||
process_group_id, _ = os.path.split(process_model_id)
|
process_group_id, _ = os.path.split(process_model_id)
|
||||||
|
|
|
@ -15,27 +15,6 @@ from spiffworkflow_backend.routes.tasks_controller import _dequeued_interstitial
|
||||||
class TestForGoodErrors(BaseTest):
|
class TestForGoodErrors(BaseTest):
|
||||||
"""Assure when certain errors happen when rendering a jinaj2 error that it makes some sense."""
|
"""Assure when certain errors happen when rendering a jinaj2 error that it makes some sense."""
|
||||||
|
|
||||||
def get_next_user_task(
|
|
||||||
self,
|
|
||||||
process_instance_id: int,
|
|
||||||
client: FlaskClient,
|
|
||||||
with_super_admin_user: UserModel,
|
|
||||||
) -> Any:
|
|
||||||
# Call this to assure all engine-steps are fully processed before we search for human tasks.
|
|
||||||
_dequeued_interstitial_stream(process_instance_id)
|
|
||||||
|
|
||||||
"""Returns the next available user task for a given process instance, if possible."""
|
|
||||||
human_tasks = (
|
|
||||||
db.session.query(HumanTaskModel).filter(HumanTaskModel.process_instance_id == process_instance_id).all()
|
|
||||||
)
|
|
||||||
assert len(human_tasks) > 0, "No human tasks found for process."
|
|
||||||
human_task = human_tasks[0]
|
|
||||||
response = client.get(
|
|
||||||
f"/v1.0/tasks/{process_instance_id}/{human_task.task_id}",
|
|
||||||
headers=self.logged_in_headers(with_super_admin_user),
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def test_invalid_form(
|
def test_invalid_form(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
|
@ -61,7 +40,7 @@ class TestForGoodErrors(BaseTest):
|
||||||
headers=self.logged_in_headers(with_super_admin_user),
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
response = self.get_next_user_task(process_instance_id, client, with_super_admin_user)
|
response = self._get_next_user_task(process_instance_id, client, with_super_admin_user)
|
||||||
assert response.json is not None
|
assert response.json is not None
|
||||||
assert response.json["error_type"] == "TemplateSyntaxError"
|
assert response.json["error_type"] == "TemplateSyntaxError"
|
||||||
assert response.json["line_number"] == 3
|
assert response.json["line_number"] == 3
|
||||||
|
@ -88,7 +67,7 @@ class TestForGoodErrors(BaseTest):
|
||||||
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance.id}/run",
|
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance.id}/run",
|
||||||
headers=self.logged_in_headers(with_super_admin_user),
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
)
|
)
|
||||||
response = self.get_next_user_task(process_instance.id, client, with_super_admin_user)
|
response = self._get_next_user_task(process_instance.id, client, with_super_admin_user)
|
||||||
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert response.json is not None
|
assert response.json is not None
|
||||||
|
@ -99,3 +78,24 @@ class TestForGoodErrors(BaseTest):
|
||||||
assert "instructions for end user" in response.json["message"]
|
assert "instructions for end user" in response.json["message"]
|
||||||
assert "Jinja2" in response.json["message"]
|
assert "Jinja2" in response.json["message"]
|
||||||
assert "unexpected '='" in response.json["message"]
|
assert "unexpected '='" in response.json["message"]
|
||||||
|
|
||||||
|
def _get_next_user_task(
|
||||||
|
self,
|
||||||
|
process_instance_id: int,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
|
) -> Any:
|
||||||
|
# Call this to assure all engine-steps are fully processed before we search for human tasks.
|
||||||
|
_dequeued_interstitial_stream(process_instance_id)
|
||||||
|
|
||||||
|
"""Returns the next available user task for a given process instance, if possible."""
|
||||||
|
human_tasks = (
|
||||||
|
db.session.query(HumanTaskModel).filter(HumanTaskModel.process_instance_id == process_instance_id).all()
|
||||||
|
)
|
||||||
|
assert len(human_tasks) > 0, "No human tasks found for process."
|
||||||
|
human_task = human_tasks[0]
|
||||||
|
response = client.get(
|
||||||
|
f"/v1.0/tasks/{process_instance_id}/{human_task.task_id}",
|
||||||
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
|
@ -3,6 +3,7 @@ import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
from hashlib import sha256
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
@ -232,7 +233,6 @@ class TestProcessApi(BaseTest):
|
||||||
with_db_and_bpmn_file_cleanup: None,
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
with_super_admin_user: UserModel,
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_primary_process_id_updates_via_xml."""
|
|
||||||
process_group_id = "test_group"
|
process_group_id = "test_group"
|
||||||
process_model_id = "sample"
|
process_model_id = "sample"
|
||||||
process_model_identifier = f"{process_group_id}/{process_model_id}"
|
process_model_identifier = f"{process_group_id}/{process_model_id}"
|
||||||
|
@ -258,10 +258,11 @@ class TestProcessApi(BaseTest):
|
||||||
updated_bpmn_file_data_string = bpmn_file_data_string.replace(old_string, new_string)
|
updated_bpmn_file_data_string = bpmn_file_data_string.replace(old_string, new_string)
|
||||||
updated_bpmn_file_data_bytes = bytearray(updated_bpmn_file_data_string, "utf-8")
|
updated_bpmn_file_data_bytes = bytearray(updated_bpmn_file_data_string, "utf-8")
|
||||||
data = {"file": (io.BytesIO(updated_bpmn_file_data_bytes), bpmn_file_name)}
|
data = {"file": (io.BytesIO(updated_bpmn_file_data_bytes), bpmn_file_name)}
|
||||||
|
file_contents_hash = sha256(bpmn_file_data_bytes).hexdigest()
|
||||||
|
|
||||||
modified_process_model_id = process_model_identifier.replace("/", ":")
|
modified_process_model_id = process_model_identifier.replace("/", ":")
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/v1.0/process-models/{modified_process_model_id}/files/{bpmn_file_name}",
|
f"/v1.0/process-models/{modified_process_model_id}/files/{bpmn_file_name}?file_contents_hash={file_contents_hash}",
|
||||||
data=data,
|
data=data,
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
|
@ -780,13 +781,12 @@ class TestProcessApi(BaseTest):
|
||||||
with_db_and_bpmn_file_cleanup: None,
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
with_super_admin_user: UserModel,
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_process_model_file_update."""
|
|
||||||
process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
|
process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
|
||||||
modified_process_model_id = process_model_identifier.replace("/", ":")
|
modified_process_model_id = process_model_identifier.replace("/", ":")
|
||||||
|
|
||||||
data = {"key1": "THIS DATA"}
|
data = {"key1": "THIS DATA"}
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg",
|
f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg?file_contents_hash=does_not_matter",
|
||||||
data=data,
|
data=data,
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
|
@ -803,13 +803,12 @@ class TestProcessApi(BaseTest):
|
||||||
with_db_and_bpmn_file_cleanup: None,
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
with_super_admin_user: UserModel,
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_process_model_file_update."""
|
|
||||||
process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
|
process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
|
||||||
modified_process_model_id = process_model_identifier.replace("/", ":")
|
modified_process_model_id = process_model_identifier.replace("/", ":")
|
||||||
|
|
||||||
data = {"file": (io.BytesIO(b""), "random_fact.svg")}
|
data = {"file": (io.BytesIO(b""), "random_fact.svg")}
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg",
|
f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg?file_contents_hash=does_not_matter",
|
||||||
data=data,
|
data=data,
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
|
@ -827,30 +826,22 @@ class TestProcessApi(BaseTest):
|
||||||
with_db_and_bpmn_file_cleanup: None,
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
with_super_admin_user: UserModel,
|
with_super_admin_user: UserModel,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_process_model_file_update."""
|
|
||||||
process_group_id = "test_group"
|
process_group_id = "test_group"
|
||||||
process_group_description = "Test Group"
|
process_model_id = "simple_form"
|
||||||
process_model_id = "random_fact"
|
|
||||||
process_model_identifier = f"{process_group_id}/{process_model_id}"
|
process_model_identifier = f"{process_group_id}/{process_model_id}"
|
||||||
self.create_process_group_with_api(client, with_super_admin_user, process_group_id, process_group_description)
|
bpmn_file_name = "simple_form.json"
|
||||||
self.create_process_model_with_api(
|
load_test_spec(
|
||||||
client,
|
|
||||||
process_model_id=process_model_identifier,
|
process_model_id=process_model_identifier,
|
||||||
user=with_super_admin_user,
|
process_model_source_directory="simple_form",
|
||||||
)
|
|
||||||
|
|
||||||
bpmn_file_name = "random_fact.bpmn"
|
|
||||||
original_file = load_test_spec(
|
|
||||||
process_model_id=process_model_id,
|
|
||||||
bpmn_file_name=bpmn_file_name,
|
|
||||||
process_model_source_directory="random_fact",
|
|
||||||
)
|
)
|
||||||
|
bpmn_file_data_bytes = self.get_test_data_file_contents(bpmn_file_name, process_model_id)
|
||||||
|
file_contents_hash = sha256(bpmn_file_data_bytes).hexdigest()
|
||||||
|
|
||||||
modified_process_model_id = process_model_identifier.replace("/", ":")
|
modified_process_model_id = process_model_identifier.replace("/", ":")
|
||||||
new_file_contents = b"THIS_IS_NEW_DATA"
|
new_file_contents = b"THIS_IS_NEW_DATA"
|
||||||
data = {"file": (io.BytesIO(new_file_contents), "random_fact.svg")}
|
data = {"file": (io.BytesIO(new_file_contents), bpmn_file_name)}
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg",
|
f"/v1.0/process-models/{modified_process_model_id}/files/{bpmn_file_name}?file_contents_hash={file_contents_hash}",
|
||||||
data=data,
|
data=data,
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
|
@ -862,12 +853,11 @@ class TestProcessApi(BaseTest):
|
||||||
assert response.json["file_contents"] is not None
|
assert response.json["file_contents"] is not None
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg",
|
f"/v1.0/process-models/{modified_process_model_id}/files/simple_form.json",
|
||||||
headers=self.logged_in_headers(with_super_admin_user),
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
updated_file = json.loads(response.get_data(as_text=True))
|
updated_file = json.loads(response.get_data(as_text=True))
|
||||||
assert original_file != updated_file
|
|
||||||
assert updated_file["file_contents"] == new_file_contents.decode()
|
assert updated_file["file_contents"] == new_file_contents.decode()
|
||||||
|
|
||||||
def test_process_model_file_delete_when_bad_process_model(
|
def test_process_model_file_delete_when_bad_process_model(
|
||||||
|
@ -879,9 +869,6 @@ class TestProcessApi(BaseTest):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_process_model_file_update."""
|
"""Test_process_model_file_update."""
|
||||||
process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
|
process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
|
||||||
# self.create_spec_file(client, user=with_super_admin_user)
|
|
||||||
|
|
||||||
# process_model = load_test_spec("random_fact")
|
|
||||||
bad_process_model_identifier = f"x{process_model_identifier}"
|
bad_process_model_identifier = f"x{process_model_identifier}"
|
||||||
modified_bad_process_model_identifier = bad_process_model_identifier.replace("/", ":")
|
modified_bad_process_model_identifier = bad_process_model_identifier.replace("/", ":")
|
||||||
response = client.delete(
|
response = client.delete(
|
||||||
|
|
|
@ -434,6 +434,56 @@ class TestProcessInstanceProcessor(BaseTest):
|
||||||
|
|
||||||
assert process_instance.status == "complete"
|
assert process_instance.status == "complete"
|
||||||
|
|
||||||
|
def test_properly_resets_process_on_tasks_with_boundary_events(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
|
) -> None:
|
||||||
|
self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group")
|
||||||
|
process_model = load_test_spec(
|
||||||
|
process_model_id="test_group/boundary_event_reset",
|
||||||
|
process_model_source_directory="boundary_event_reset",
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
assert len(process_instance.active_human_tasks) == 1
|
||||||
|
human_task_one = process_instance.active_human_tasks[0]
|
||||||
|
spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id))
|
||||||
|
ProcessInstanceService.complete_form_task(
|
||||||
|
processor, spiff_manual_task, {}, with_super_admin_user, human_task_one
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
len(process_instance.active_human_tasks) == 1
|
||||||
|
), "expected 1 active human tasks after 2nd one is completed"
|
||||||
|
assert process_instance.active_human_tasks[0].task_title == "Final"
|
||||||
|
|
||||||
|
# Reset the process back to the task within the call activity that contains a timer_boundary event.
|
||||||
|
reset_to_spiff_task: SpiffTask = processor.__class__.get_task_by_bpmn_identifier(
|
||||||
|
"manual_task_1", processor.bpmn_process_instance
|
||||||
|
)
|
||||||
|
processor.suspend()
|
||||||
|
processor = ProcessInstanceProcessor(process_instance)
|
||||||
|
ProcessInstanceProcessor.reset_process(process_instance, str(reset_to_spiff_task.id))
|
||||||
|
human_task_one = process_instance.active_human_tasks[0]
|
||||||
|
assert human_task_one.task_title == "Manual Task #1"
|
||||||
|
processor = ProcessInstanceProcessor(process_instance)
|
||||||
|
processor.manual_complete_task(str(spiff_manual_task.id), execute=True)
|
||||||
|
processor = ProcessInstanceProcessor(process_instance)
|
||||||
|
processor.resume()
|
||||||
|
processor.do_engine_steps(save=True)
|
||||||
|
process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first()
|
||||||
|
|
||||||
|
assert len(process_instance.active_human_tasks) == 1
|
||||||
|
assert process_instance.active_human_tasks[0].task_title == "Final", (
|
||||||
|
"once we reset, resume, and complete the task, we should be back to the Final step again, and not"
|
||||||
|
"stuck waiting for the call activity to complete (which was happening in a bug I'm fixing right now)"
|
||||||
|
)
|
||||||
|
|
||||||
def test_properly_saves_tasks_when_running(
|
def test_properly_saves_tasks_when_running(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
|
|
|
@ -120,6 +120,7 @@ export interface ProcessFile {
|
||||||
size: number;
|
size: number;
|
||||||
type: string;
|
type: string;
|
||||||
file_contents?: string;
|
file_contents?: string;
|
||||||
|
file_contents_hash?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProcessInstanceMetadata {
|
export interface ProcessInstanceMetadata {
|
||||||
|
|
|
@ -733,7 +733,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
||||||
if ('payload' in eventToSend)
|
if ('payload' in eventToSend)
|
||||||
eventToSend.payload = JSON.parse(eventPayload);
|
eventToSend.payload = JSON.parse(eventPayload);
|
||||||
HttpService.makeCallToBackend({
|
HttpService.makeCallToBackend({
|
||||||
path: `/send-event/${modifiedProcessModelId}/${params.process_instance_id}`,
|
path: targetUris.processInstanceSendEventPath,
|
||||||
httpMethod: 'POST',
|
httpMethod: 'POST',
|
||||||
successCallback: saveTaskDataResult,
|
successCallback: saveTaskDataResult,
|
||||||
failureCallback: addError,
|
failureCallback: addError,
|
||||||
|
|
|
@ -238,6 +238,9 @@ export default function ProcessModelEditDiagram() {
|
||||||
httpMethod = 'POST';
|
httpMethod = 'POST';
|
||||||
} else {
|
} else {
|
||||||
url += `/${fileNameWithExtension}`;
|
url += `/${fileNameWithExtension}`;
|
||||||
|
if (processModelFile && processModelFile.file_contents_hash) {
|
||||||
|
url += `?file_contents_hash=${processModelFile.file_contents_hash}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!fileNameWithExtension) {
|
if (!fileNameWithExtension) {
|
||||||
handleShowFileNameEditor();
|
handleShowFileNameEditor();
|
||||||
|
|
Loading…
Reference in New Issue