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:
|
||||
operationId: spiffworkflow_backend.routes.process_models_controller.process_model_file_update
|
||||
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:
|
||||
- Process Model Files
|
||||
requestBody:
|
||||
|
@ -1632,9 +1639,9 @@ paths:
|
|||
required: true
|
||||
description: The unique id of the process instance
|
||||
schema:
|
||||
type: string
|
||||
type: integer
|
||||
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
|
||||
tags:
|
||||
- Process Instances
|
||||
|
@ -1823,6 +1830,27 @@ paths:
|
|||
schema:
|
||||
$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:
|
||||
parameters:
|
||||
- name: process_instance_id
|
||||
|
|
|
@ -127,8 +127,8 @@ class ApiError(Exception):
|
|||
instance.task_trace = TaskModelError.get_task_trace(task_model)
|
||||
|
||||
try:
|
||||
spec_reference = TaskService.get_spec_reference_from_bpmn_process(task_model.bpmn_process)
|
||||
instance.file_name = spec_reference.file_name
|
||||
spec_reference_filename = TaskService.get_spec_filename_from_bpmn_process(task_model.bpmn_process)
|
||||
instance.file_name = spec_reference_filename
|
||||
except Exception as exception:
|
||||
current_app.logger.error(exception)
|
||||
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
"""File."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import marshmallow
|
||||
from marshmallow import INCLUDE
|
||||
from marshmallow import Schema
|
||||
from typing import Any
|
||||
|
||||
from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum
|
||||
from spiffworkflow_backend.models.spec_reference import SpecReference
|
||||
|
@ -74,10 +71,10 @@ class File:
|
|||
type: str
|
||||
last_modified: datetime
|
||||
size: int
|
||||
references: Optional[list[SpecReference]] = None
|
||||
file_contents: Optional[bytes] = None
|
||||
process_model_id: Optional[str] = None
|
||||
process_group_id: Optional[str] = None
|
||||
references: list[SpecReference] | None = None
|
||||
file_contents: bytes | None = None
|
||||
process_model_id: str | None = None
|
||||
file_contents_hash: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""__post_init__."""
|
||||
|
@ -91,7 +88,7 @@ class File:
|
|||
content_type: str,
|
||||
last_modified: datetime,
|
||||
file_size: int,
|
||||
) -> "File":
|
||||
) -> File:
|
||||
"""From_file_system."""
|
||||
instance = cls(
|
||||
name=file_name,
|
||||
|
@ -102,28 +99,9 @@ class File:
|
|||
)
|
||||
return instance
|
||||
|
||||
|
||||
class FileSchema(Schema):
|
||||
"""FileSchema."""
|
||||
|
||||
class Meta:
|
||||
"""Meta."""
|
||||
|
||||
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"))
|
||||
@property
|
||||
def serialized(self) -> dict[str, Any]:
|
||||
dictionary = self.__dict__
|
||||
if isinstance(self.file_contents, bytes):
|
||||
dictionary["file_contents"] = self.file_contents.decode("utf-8")
|
||||
return dictionary
|
||||
|
|
|
@ -87,7 +87,7 @@ class ProcessModelInfoSchema(Schema):
|
|||
display_order = marshmallow.fields.Integer(allow_none=True)
|
||||
primary_file_name = 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()
|
||||
exception_notification_addresses = marshmallow.fields.List(marshmallow.fields.String)
|
||||
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 (
|
||||
ProcessInstanceProcessor,
|
||||
)
|
||||
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
"""Commit_and_push_to_git."""
|
||||
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(
|
||||
process_model_identifier: Optional[str] = None,
|
||||
) -> flask.wrappers.Response:
|
||||
"""Process_instance_report_column_list."""
|
||||
table_columns = ProcessInstanceReportService.builtin_column_options()
|
||||
system_report_column_options = ProcessInstanceReportService.system_report_column_options()
|
||||
columns_for_metadata_query = (
|
||||
db.session.query(ProcessInstanceMetadataModel.key)
|
||||
.order_by(ProcessInstanceMetadataModel.key)
|
||||
|
@ -315,7 +315,7 @@ def process_instance_report_column_list(
|
|||
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)
|
||||
return make_response(jsonify(table_columns + system_report_column_options + columns_for_metadata_strings), 200)
|
||||
|
||||
|
||||
def process_instance_show_for_me(
|
||||
|
@ -642,6 +642,38 @@ def process_instance_find_by_id(
|
|||
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(
|
||||
modified_process_model_identifier: str,
|
||||
process_instance: ProcessInstanceModel,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
from hashlib import sha256
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
@ -18,7 +19,6 @@ from werkzeug.datastructures import FileStorage
|
|||
|
||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||
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_instance_report import (
|
||||
ProcessInstanceReportModel,
|
||||
|
@ -245,10 +245,13 @@ def process_model_list(
|
|||
return make_response(jsonify(response_json), 200)
|
||||
|
||||
|
||||
def process_model_file_update(modified_process_model_identifier: str, file_name: str) -> flask.wrappers.Response:
|
||||
"""Process_model_file_update."""
|
||||
def 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"
|
||||
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:
|
||||
|
@ -293,24 +296,23 @@ def process_model_file_create(
|
|||
|
||||
|
||||
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 = _get_process_model(process_model_identifier)
|
||||
files = SpecFileService.get_files(process_model, file_name)
|
||||
if len(files) == 0:
|
||||
raise ApiError(
|
||||
error_code="unknown file",
|
||||
message=(
|
||||
f"No information exists for file {file_name} it does not exist in workflow {process_model_identifier}."
|
||||
),
|
||||
error_code="process_model_file_not_found",
|
||||
message=f"File {file_name} not found in workflow {process_model_identifier}.",
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
file = files[0]
|
||||
file_contents = SpecFileService.get_data(process_model, file.name)
|
||||
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
|
||||
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,
|
||||
message_for_git_commit: str,
|
||||
http_status_to_return: int,
|
||||
file_contents_hash: Optional[str] = None,
|
||||
) -> flask.wrappers.Response:
|
||||
"""_create_or_update_process_model_file."""
|
||||
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
||||
|
@ -498,6 +501,21 @@ def _create_or_update_process_model_file(
|
|||
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
|
||||
try:
|
||||
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
|
||||
_commit_and_push_to_git(f"{message_for_git_commit} {process_model_identifier}/{file.name}")
|
||||
|
||||
return Response(
|
||||
json.dumps(FileSchema().dump(file)),
|
||||
status=http_status_to_return,
|
||||
mimetype="application/json",
|
||||
)
|
||||
return make_response(jsonify(file), http_status_to_return)
|
||||
|
|
|
@ -532,7 +532,9 @@ class AuthorizationService:
|
|||
# 1. view your own instances.
|
||||
# 2. view the logs for these instances.
|
||||
if permission_set == "start":
|
||||
target_uri = f"/process-instances/{process_related_path_segment}"
|
||||
path_prefixes_that_allow_create_access = ["process-instances"]
|
||||
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
|
||||
|
|
|
@ -1115,59 +1115,54 @@ class ProcessInstanceProcessor:
|
|||
|
||||
def manual_complete_task(self, task_id: str, execute: bool) -> None:
|
||||
"""Mark the task complete optionally executing it."""
|
||||
spiff_tasks_updated = {}
|
||||
start_in_seconds = time.time()
|
||||
spiff_task = self.bpmn_process_instance.get_task_from_id(UUID(task_id))
|
||||
event_type = ProcessInstanceEventType.task_skipped.value
|
||||
start_time = time.time()
|
||||
|
||||
if execute:
|
||||
current_app.logger.info(
|
||||
f"Manually executing Task {spiff_task.task_spec.name} of process"
|
||||
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):
|
||||
subprocess = self.bpmn_process_instance.get_subprocess(spiff_task)
|
||||
# We have to get to the actual start event
|
||||
for task in self.bpmn_process_instance.get_tasks(workflow=subprocess):
|
||||
task.complete()
|
||||
spiff_tasks_updated[task.id] = task
|
||||
if isinstance(task.task_spec, StartEvent):
|
||||
for spiff_task in self.bpmn_process_instance.get_tasks(workflow=subprocess):
|
||||
spiff_task.run()
|
||||
if isinstance(spiff_task.task_spec, StartEvent):
|
||||
break
|
||||
else:
|
||||
spiff_task.complete()
|
||||
spiff_tasks_updated[spiff_task.id] = spiff_task
|
||||
for child in spiff_task.children:
|
||||
spiff_tasks_updated[child.id] = child
|
||||
spiff_task.run()
|
||||
event_type = ProcessInstanceEventType.task_executed_manually.value
|
||||
else:
|
||||
spiff_logger = logging.getLogger("spiff")
|
||||
spiff_logger.info(f"Skipped task {spiff_task.task_spec.name}", extra=spiff_task.log_info())
|
||||
spiff_task._set_state(TaskState.COMPLETED)
|
||||
for child in spiff_task.children:
|
||||
child.task_spec._update(child)
|
||||
spiff_tasks_updated[child.id] = child
|
||||
spiff_task.complete()
|
||||
spiff_task.workflow.last_task = spiff_task
|
||||
spiff_tasks_updated[spiff_task.id] = spiff_task
|
||||
|
||||
end_in_seconds = time.time()
|
||||
|
||||
if isinstance(spiff_task.task_spec, EndEvent):
|
||||
for task in self.bpmn_process_instance.get_tasks(TaskState.DEFINITE_MASK, workflow=spiff_task.workflow):
|
||||
task.complete()
|
||||
spiff_tasks_updated[task.id] = task
|
||||
|
||||
# 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.
|
||||
for task in self.bpmn_process_instance.get_tasks(TaskState.READY):
|
||||
if isinstance(task.task_spec, SubWorkflowTask):
|
||||
task.complete()
|
||||
spiff_tasks_updated[task.id] = task
|
||||
|
||||
task_service = TaskService(
|
||||
process_instance=self.process_instance_model,
|
||||
serializer=self._serializer,
|
||||
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():
|
||||
(
|
||||
bpmn_process,
|
||||
|
@ -1216,6 +1211,14 @@ class ProcessInstanceProcessor:
|
|||
raise TaskNotFoundError(
|
||||
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
|
||||
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()
|
||||
|
||||
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.suspend()
|
||||
|
||||
|
|
|
@ -320,7 +320,7 @@ class ProcessInstanceReportService:
|
|||
|
||||
@classmethod
|
||||
def builtin_column_options(cls) -> list[ReportMetadataColumn]:
|
||||
"""Builtin_column_options."""
|
||||
"""Columns that are actually in the process instance table."""
|
||||
return_value: list[ReportMetadataColumn] = [
|
||||
{"Header": "Id", "accessor": "id", "filterable": False},
|
||||
{
|
||||
|
@ -339,6 +339,15 @@ class ProcessInstanceReportService:
|
|||
]
|
||||
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
|
||||
def get_filter_value(cls, filters: list[FilterValue], filter_key: str) -> Any:
|
||||
for filter in filters:
|
||||
|
|
|
@ -175,7 +175,6 @@ class SpecFileService(FileSystemService):
|
|||
|
||||
@classmethod
|
||||
def update_file(cls, process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes) -> File:
|
||||
"""Update_file."""
|
||||
SpecFileService.assert_valid_file_name(file_name)
|
||||
cls.validate_bpmn_xml(file_name, binary_data)
|
||||
|
||||
|
|
|
@ -89,15 +89,15 @@ class TaskModelError(Exception):
|
|||
task_definition = task_model.task_definition
|
||||
task_bpmn_name = TaskService.get_name_for_display(task_definition)
|
||||
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:
|
||||
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()
|
||||
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(
|
||||
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
|
||||
|
||||
|
@ -630,6 +630,15 @@ class TaskService:
|
|||
result.append({"event": event_definition, "label": extensions["signalButtonLabel"]})
|
||||
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
|
||||
def get_spec_reference_from_bpmn_process(cls, bpmn_process: BpmnProcessModel) -> SpecReferenceCache:
|
||||
"""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,
|
||||
display_name: str = "",
|
||||
) -> str:
|
||||
"""Create_process_group."""
|
||||
process_group = ProcessGroup(id=process_group_id, display_name=display_name, display_order=0, admin=False)
|
||||
response = client.post(
|
||||
"/v1.0/process-groups",
|
||||
|
@ -138,7 +137,6 @@ class BaseTest:
|
|||
primary_file_name: Optional[str] = None,
|
||||
user: Optional[UserModel] = None,
|
||||
) -> TestResponse:
|
||||
"""Create_process_model."""
|
||||
if process_model_id is not None:
|
||||
# make sure we have a group
|
||||
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):
|
||||
"""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(
|
||||
self,
|
||||
app: Flask,
|
||||
|
@ -61,7 +40,7 @@ class TestForGoodErrors(BaseTest):
|
|||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
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["error_type"] == "TemplateSyntaxError"
|
||||
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",
|
||||
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.json is not None
|
||||
|
@ -99,3 +78,24 @@ class TestForGoodErrors(BaseTest):
|
|||
assert "instructions for end user" in response.json["message"]
|
||||
assert "Jinja2" 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 os
|
||||
import time
|
||||
from hashlib import sha256
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
|
||||
|
@ -232,7 +233,6 @@ class TestProcessApi(BaseTest):
|
|||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
"""Test_primary_process_id_updates_via_xml."""
|
||||
process_group_id = "test_group"
|
||||
process_model_id = "sample"
|
||||
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_bytes = bytearray(updated_bpmn_file_data_string, "utf-8")
|
||||
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("/", ":")
|
||||
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,
|
||||
follow_redirects=True,
|
||||
content_type="multipart/form-data",
|
||||
|
@ -780,13 +781,12 @@ class TestProcessApi(BaseTest):
|
|||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
"""Test_process_model_file_update."""
|
||||
process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
|
||||
modified_process_model_id = process_model_identifier.replace("/", ":")
|
||||
|
||||
data = {"key1": "THIS DATA"}
|
||||
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,
|
||||
follow_redirects=True,
|
||||
content_type="multipart/form-data",
|
||||
|
@ -803,13 +803,12 @@ class TestProcessApi(BaseTest):
|
|||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
"""Test_process_model_file_update."""
|
||||
process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
|
||||
modified_process_model_id = process_model_identifier.replace("/", ":")
|
||||
|
||||
data = {"file": (io.BytesIO(b""), "random_fact.svg")}
|
||||
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,
|
||||
follow_redirects=True,
|
||||
content_type="multipart/form-data",
|
||||
|
@ -827,30 +826,22 @@ class TestProcessApi(BaseTest):
|
|||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
"""Test_process_model_file_update."""
|
||||
process_group_id = "test_group"
|
||||
process_group_description = "Test Group"
|
||||
process_model_id = "random_fact"
|
||||
process_model_id = "simple_form"
|
||||
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)
|
||||
self.create_process_model_with_api(
|
||||
client,
|
||||
bpmn_file_name = "simple_form.json"
|
||||
load_test_spec(
|
||||
process_model_id=process_model_identifier,
|
||||
user=with_super_admin_user,
|
||||
)
|
||||
|
||||
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",
|
||||
process_model_source_directory="simple_form",
|
||||
)
|
||||
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("/", ":")
|
||||
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(
|
||||
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,
|
||||
follow_redirects=True,
|
||||
content_type="multipart/form-data",
|
||||
|
@ -862,12 +853,11 @@ class TestProcessApi(BaseTest):
|
|||
assert response.json["file_contents"] is not None
|
||||
|
||||
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),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
updated_file = json.loads(response.get_data(as_text=True))
|
||||
assert original_file != updated_file
|
||||
assert updated_file["file_contents"] == new_file_contents.decode()
|
||||
|
||||
def test_process_model_file_delete_when_bad_process_model(
|
||||
|
@ -879,9 +869,6 @@ class TestProcessApi(BaseTest):
|
|||
) -> None:
|
||||
"""Test_process_model_file_update."""
|
||||
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}"
|
||||
modified_bad_process_model_identifier = bad_process_model_identifier.replace("/", ":")
|
||||
response = client.delete(
|
||||
|
|
|
@ -434,6 +434,56 @@ class TestProcessInstanceProcessor(BaseTest):
|
|||
|
||||
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(
|
||||
self,
|
||||
app: Flask,
|
||||
|
|
|
@ -120,6 +120,7 @@ export interface ProcessFile {
|
|||
size: number;
|
||||
type: string;
|
||||
file_contents?: string;
|
||||
file_contents_hash?: string;
|
||||
}
|
||||
|
||||
export interface ProcessInstanceMetadata {
|
||||
|
|
|
@ -733,7 +733,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
if ('payload' in eventToSend)
|
||||
eventToSend.payload = JSON.parse(eventPayload);
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/send-event/${modifiedProcessModelId}/${params.process_instance_id}`,
|
||||
path: targetUris.processInstanceSendEventPath,
|
||||
httpMethod: 'POST',
|
||||
successCallback: saveTaskDataResult,
|
||||
failureCallback: addError,
|
||||
|
|
|
@ -238,6 +238,9 @@ export default function ProcessModelEditDiagram() {
|
|||
httpMethod = 'POST';
|
||||
} else {
|
||||
url += `/${fileNameWithExtension}`;
|
||||
if (processModelFile && processModelFile.file_contents_hash) {
|
||||
url += `?file_contents_hash=${processModelFile.file_contents_hash}`;
|
||||
}
|
||||
}
|
||||
if (!fileNameWithExtension) {
|
||||
handleShowFileNameEditor();
|
||||
|
|
Loading…
Reference in New Issue