mirror of
https://github.com/status-im/spiff-arena.git
synced 2025-02-06 07:04:18 +00:00
fixed test w/ burnettk
This commit is contained in:
parent
d73baedcbe
commit
b1568fb472
@ -1,9 +1,7 @@
|
|||||||
"""APIs for dealing with process groups, process models, and process instances."""
|
"""APIs for dealing with process groups, process models, and process instances."""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
|
||||||
from sys import exc_info
|
from sys import exc_info
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
@ -14,10 +12,11 @@ from typing import Union
|
|||||||
import flask.wrappers
|
import flask.wrappers
|
||||||
import jinja2
|
import jinja2
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from flask import current_app, stream_with_context
|
from flask import current_app
|
||||||
from flask import g
|
from flask import g
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
from flask import make_response
|
from flask import make_response
|
||||||
|
from flask import stream_with_context
|
||||||
from flask.wrappers import Response
|
from flask.wrappers import Response
|
||||||
from jinja2 import TemplateSyntaxError
|
from jinja2 import TemplateSyntaxError
|
||||||
from SpiffWorkflow.exceptions import WorkflowTaskException # type: ignore
|
from SpiffWorkflow.exceptions import WorkflowTaskException # type: ignore
|
||||||
@ -85,7 +84,7 @@ class ReactJsonSchemaSelectOption(TypedDict):
|
|||||||
|
|
||||||
|
|
||||||
def task_list_my_tasks(
|
def task_list_my_tasks(
|
||||||
process_instance_id: Optional[int] = None, page: int = 1, per_page: int = 100
|
process_instance_id: Optional[int] = None, page: int = 1, per_page: int = 100
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
"""Task_list_my_tasks."""
|
"""Task_list_my_tasks."""
|
||||||
principal = _find_principal_or_raise()
|
principal = _find_principal_or_raise()
|
||||||
@ -166,7 +165,7 @@ def task_list_for_me(page: int = 1, per_page: int = 100) -> flask.wrappers.Respo
|
|||||||
|
|
||||||
|
|
||||||
def task_list_for_my_groups(
|
def task_list_for_my_groups(
|
||||||
user_group_identifier: Optional[str] = None, page: int = 1, per_page: int = 100
|
user_group_identifier: Optional[str] = None, page: int = 1, per_page: int = 100
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
"""Task_list_for_my_groups."""
|
"""Task_list_for_my_groups."""
|
||||||
return _get_tasks(
|
return _get_tasks(
|
||||||
@ -178,9 +177,9 @@ def task_list_for_my_groups(
|
|||||||
|
|
||||||
|
|
||||||
def task_data_show(
|
def task_data_show(
|
||||||
modified_process_model_identifier: str,
|
modified_process_model_identifier: str,
|
||||||
process_instance_id: int,
|
process_instance_id: int,
|
||||||
task_guid: str,
|
task_guid: str,
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
task_model = _get_task_model_from_guid_or_raise(task_guid, process_instance_id)
|
task_model = _get_task_model_from_guid_or_raise(task_guid, process_instance_id)
|
||||||
task_model.data = task_model.json_data()
|
task_model.data = task_model.json_data()
|
||||||
@ -188,10 +187,10 @@ def task_data_show(
|
|||||||
|
|
||||||
|
|
||||||
def task_data_update(
|
def task_data_update(
|
||||||
process_instance_id: str,
|
process_instance_id: str,
|
||||||
modified_process_model_identifier: str,
|
modified_process_model_identifier: str,
|
||||||
task_guid: str,
|
task_guid: str,
|
||||||
body: Dict,
|
body: Dict,
|
||||||
) -> Response:
|
) -> Response:
|
||||||
"""Update task data."""
|
"""Update task data."""
|
||||||
process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first()
|
process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first()
|
||||||
@ -241,10 +240,10 @@ def task_data_update(
|
|||||||
|
|
||||||
|
|
||||||
def manual_complete_task(
|
def manual_complete_task(
|
||||||
modified_process_model_identifier: str,
|
modified_process_model_identifier: str,
|
||||||
process_instance_id: str,
|
process_instance_id: str,
|
||||||
task_guid: str,
|
task_guid: str,
|
||||||
body: Dict,
|
body: Dict,
|
||||||
) -> Response:
|
) -> Response:
|
||||||
"""Mark a task complete without executing it."""
|
"""Mark a task complete without executing it."""
|
||||||
execute = body.get("execute", True)
|
execute = body.get("execute", True)
|
||||||
@ -359,9 +358,7 @@ def _render_instructions_for_end_user(spiff_task: SpiffTask, task: Task):
|
|||||||
if task.properties and "instructionsForEndUser" in task.properties:
|
if task.properties and "instructionsForEndUser" in task.properties:
|
||||||
if task.properties["instructionsForEndUser"]:
|
if task.properties["instructionsForEndUser"]:
|
||||||
try:
|
try:
|
||||||
instructions = _render_jinja_template(
|
instructions = _render_jinja_template(task.properties["instructionsForEndUser"], spiff_task)
|
||||||
task.properties["instructionsForEndUser"], spiff_task
|
|
||||||
)
|
|
||||||
task.properties["instructionsForEndUser"] = instructions
|
task.properties["instructionsForEndUser"] = instructions
|
||||||
return instructions
|
return instructions
|
||||||
except WorkflowTaskException as wfe:
|
except WorkflowTaskException as wfe:
|
||||||
@ -371,9 +368,9 @@ def _render_instructions_for_end_user(spiff_task: SpiffTask, task: Task):
|
|||||||
|
|
||||||
|
|
||||||
def process_data_show(
|
def process_data_show(
|
||||||
process_instance_id: int,
|
process_instance_id: int,
|
||||||
process_data_identifier: str,
|
process_data_identifier: str,
|
||||||
modified_process_model_identifier: str,
|
modified_process_model_identifier: str,
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
"""Process_data_show."""
|
"""Process_data_show."""
|
||||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||||
@ -408,7 +405,7 @@ def _interstitial_stream(process_instance_id: int):
|
|||||||
instructions = _render_instructions_for_end_user(spiff_task, task)
|
instructions = _render_instructions_for_end_user(spiff_task, task)
|
||||||
if instructions and spiff_task.id not in reported_ids:
|
if instructions and spiff_task.id not in reported_ids:
|
||||||
reported_ids.append(spiff_task.id)
|
reported_ids.append(spiff_task.id)
|
||||||
yield f'data: {current_app.json.dumps(task)} \n\n'
|
yield f"data: {current_app.json.dumps(task)} \n\n"
|
||||||
last_task = spiff_task
|
last_task = spiff_task
|
||||||
processor.do_engine_steps(execution_strategy_name="run_until_user_message")
|
processor.do_engine_steps(execution_strategy_name="run_until_user_message")
|
||||||
processor.do_engine_steps(execution_strategy_name="one_at_a_time")
|
processor.do_engine_steps(execution_strategy_name="one_at_a_time")
|
||||||
@ -420,15 +417,15 @@ def _interstitial_stream(process_instance_id: int):
|
|||||||
|
|
||||||
def interstitial(process_instance_id: int):
|
def interstitial(process_instance_id: int):
|
||||||
"""A Server Side Events Stream for watching the execution of engine tasks in a
|
"""A Server Side Events Stream for watching the execution of engine tasks in a
|
||||||
process instance. """
|
process instance."""
|
||||||
return Response(stream_with_context(_interstitial_stream(process_instance_id)), mimetype='text/event-stream')
|
return Response(stream_with_context(_interstitial_stream(process_instance_id)), mimetype="text/event-stream")
|
||||||
|
|
||||||
|
|
||||||
def _task_submit_shared(
|
def _task_submit_shared(
|
||||||
process_instance_id: int,
|
process_instance_id: int,
|
||||||
task_guid: str,
|
task_guid: str,
|
||||||
body: Dict[str, Any],
|
body: Dict[str, Any],
|
||||||
save_as_draft: bool = False,
|
save_as_draft: bool = False,
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
principal = _find_principal_or_raise()
|
principal = _find_principal_or_raise()
|
||||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||||
@ -509,18 +506,24 @@ def _task_submit_shared(
|
|||||||
task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
|
task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
|
||||||
return make_response(jsonify(task), 200)
|
return make_response(jsonify(task), 200)
|
||||||
|
|
||||||
return Response(json.dumps(
|
return Response(
|
||||||
{"ok": True,
|
json.dumps(
|
||||||
"process_model_identifier": process_instance.process_model_identifier,
|
{
|
||||||
"process_instance_id": process_instance_id
|
"ok": True,
|
||||||
}), status=202, mimetype="application/json")
|
"process_model_identifier": process_instance.process_model_identifier,
|
||||||
|
"process_instance_id": process_instance_id,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
status=202,
|
||||||
|
mimetype="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def task_submit(
|
def task_submit(
|
||||||
process_instance_id: int,
|
process_instance_id: int,
|
||||||
task_guid: str,
|
task_guid: str,
|
||||||
body: Dict[str, Any],
|
body: Dict[str, Any],
|
||||||
save_as_draft: bool = False,
|
save_as_draft: bool = False,
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
"""Task_submit_user_data."""
|
"""Task_submit_user_data."""
|
||||||
with sentry_sdk.start_span(op="controller_action", description="tasks_controller.task_submit"):
|
with sentry_sdk.start_span(op="controller_action", description="tasks_controller.task_submit"):
|
||||||
@ -528,11 +531,11 @@ def task_submit(
|
|||||||
|
|
||||||
|
|
||||||
def _get_tasks(
|
def _get_tasks(
|
||||||
processes_started_by_user: bool = True,
|
processes_started_by_user: bool = True,
|
||||||
has_lane_assignment_id: bool = True,
|
has_lane_assignment_id: bool = True,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
per_page: int = 100,
|
per_page: int = 100,
|
||||||
user_group_identifier: Optional[str] = None,
|
user_group_identifier: Optional[str] = None,
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
"""Get_tasks."""
|
"""Get_tasks."""
|
||||||
user_id = g.user.id
|
user_id = g.user.id
|
||||||
@ -679,9 +682,9 @@ def _render_jinja_template(unprocessed_template: str, spiff_task: SpiffTask) ->
|
|||||||
|
|
||||||
|
|
||||||
def _get_spiff_task_from_process_instance(
|
def _get_spiff_task_from_process_instance(
|
||||||
task_guid: str,
|
task_guid: str,
|
||||||
process_instance: ProcessInstanceModel,
|
process_instance: ProcessInstanceModel,
|
||||||
processor: Union[ProcessInstanceProcessor, None] = None,
|
processor: Union[ProcessInstanceProcessor, None] = None,
|
||||||
) -> SpiffTask:
|
) -> SpiffTask:
|
||||||
"""Get_spiff_task_from_process_instance."""
|
"""Get_spiff_task_from_process_instance."""
|
||||||
if processor is None:
|
if processor is None:
|
||||||
@ -737,8 +740,9 @@ def _update_form_schema_with_task_data_as_needed(in_dict: dict, task: Task, spif
|
|||||||
select_options_from_task_data = task.data.get(task_data_var)
|
select_options_from_task_data = task.data.get(task_data_var)
|
||||||
if isinstance(select_options_from_task_data, list):
|
if isinstance(select_options_from_task_data, list):
|
||||||
if all("value" in d and "label" in d for d in select_options_from_task_data):
|
if all("value" in d and "label" in d for d in select_options_from_task_data):
|
||||||
|
|
||||||
def map_function(
|
def map_function(
|
||||||
task_data_select_option: TaskDataSelectOption,
|
task_data_select_option: TaskDataSelectOption,
|
||||||
) -> ReactJsonSchemaSelectOption:
|
) -> ReactJsonSchemaSelectOption:
|
||||||
"""Map_function."""
|
"""Map_function."""
|
||||||
return {
|
return {
|
||||||
@ -776,9 +780,9 @@ def _get_potential_owner_usernames(assigned_user: AliasedClass) -> Any:
|
|||||||
|
|
||||||
|
|
||||||
def _find_human_task_or_raise(
|
def _find_human_task_or_raise(
|
||||||
process_instance_id: int,
|
process_instance_id: int,
|
||||||
task_guid: str,
|
task_guid: str,
|
||||||
only_tasks_that_can_be_completed: bool = False,
|
only_tasks_that_can_be_completed: bool = False,
|
||||||
) -> HumanTaskModel:
|
) -> HumanTaskModel:
|
||||||
if only_tasks_that_can_be_completed:
|
if only_tasks_that_can_be_completed:
|
||||||
human_task_query = HumanTaskModel.query.filter_by(
|
human_task_query = HumanTaskModel.query.filter_by(
|
||||||
|
@ -10,7 +10,8 @@ from typing import Tuple
|
|||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from flask import current_app, g
|
from flask import current_app
|
||||||
|
from flask import g
|
||||||
from SpiffWorkflow.bpmn.specs.events.IntermediateEvent import _BoundaryEventParent # type: ignore
|
from SpiffWorkflow.bpmn.specs.events.IntermediateEvent import _BoundaryEventParent # type: ignore
|
||||||
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
|
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
|
||||||
|
|
||||||
@ -26,8 +27,9 @@ from spiffworkflow_backend.models.process_instance_file_data import (
|
|||||||
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
||||||
from spiffworkflow_backend.models.task import Task
|
from spiffworkflow_backend.models.task import Task
|
||||||
from spiffworkflow_backend.models.user import UserModel
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService, HumanTaskNotFoundError, \
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
UserDoesNotHaveAccessToTaskError
|
from spiffworkflow_backend.services.authorization_service import HumanTaskNotFoundError
|
||||||
|
from spiffworkflow_backend.services.authorization_service import UserDoesNotHaveAccessToTaskError
|
||||||
from spiffworkflow_backend.services.git_service import GitCommandError
|
from spiffworkflow_backend.services.git_service import GitCommandError
|
||||||
from spiffworkflow_backend.services.git_service import GitService
|
from spiffworkflow_backend.services.git_service import GitService
|
||||||
from spiffworkflow_backend.services.process_instance_processor import (
|
from spiffworkflow_backend.services.process_instance_processor import (
|
||||||
@ -427,12 +429,13 @@ class ProcessInstanceService:
|
|||||||
# can complete it.
|
# can complete it.
|
||||||
can_complete = False
|
can_complete = False
|
||||||
try:
|
try:
|
||||||
AuthorizationService.assert_user_can_complete_spiff_task(processor.process_instance_model.id, spiff_task,
|
AuthorizationService.assert_user_can_complete_spiff_task(
|
||||||
g.user)
|
processor.process_instance_model.id, spiff_task, g.user
|
||||||
|
)
|
||||||
can_complete = True
|
can_complete = True
|
||||||
except HumanTaskNotFoundError as e:
|
except HumanTaskNotFoundError:
|
||||||
can_complete = False
|
can_complete = False
|
||||||
except UserDoesNotHaveAccessToTaskError as ude:
|
except UserDoesNotHaveAccessToTaskError:
|
||||||
can_complete = False
|
can_complete = False
|
||||||
|
|
||||||
if hasattr(spiff_task.task_spec, "spec"):
|
if hasattr(spiff_task.task_spec, "spec"):
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import time
|
import time
|
||||||
from typing import Callable, List
|
from typing import Callable
|
||||||
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
@ -50,11 +51,11 @@ class TaskModelSavingDelegate(EngineStepDelegate):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
serializer: BpmnWorkflowSerializer,
|
serializer: BpmnWorkflowSerializer,
|
||||||
process_instance: ProcessInstanceModel,
|
process_instance: ProcessInstanceModel,
|
||||||
bpmn_definition_to_task_definitions_mappings: dict,
|
bpmn_definition_to_task_definitions_mappings: dict,
|
||||||
secondary_engine_step_delegate: Optional[EngineStepDelegate] = None,
|
secondary_engine_step_delegate: Optional[EngineStepDelegate] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.secondary_engine_step_delegate = secondary_engine_step_delegate
|
self.secondary_engine_step_delegate = secondary_engine_step_delegate
|
||||||
self.process_instance = process_instance
|
self.process_instance = process_instance
|
||||||
@ -132,12 +133,12 @@ class TaskModelSavingDelegate(EngineStepDelegate):
|
|||||||
# excludes COMPLETED. the others were required to get PP1 to go to completion.
|
# excludes COMPLETED. the others were required to get PP1 to go to completion.
|
||||||
# process FUTURE tasks because Boundary events are not processed otherwise.
|
# process FUTURE tasks because Boundary events are not processed otherwise.
|
||||||
for waiting_spiff_task in bpmn_process_instance.get_tasks(
|
for waiting_spiff_task in bpmn_process_instance.get_tasks(
|
||||||
TaskState.WAITING
|
TaskState.WAITING
|
||||||
| TaskState.CANCELLED
|
| TaskState.CANCELLED
|
||||||
| TaskState.READY
|
| TaskState.READY
|
||||||
| TaskState.MAYBE
|
| TaskState.MAYBE
|
||||||
| TaskState.LIKELY
|
| TaskState.LIKELY
|
||||||
| TaskState.FUTURE
|
| TaskState.FUTURE
|
||||||
):
|
):
|
||||||
# these will be removed from the parent and then ignored
|
# these will be removed from the parent and then ignored
|
||||||
if waiting_spiff_task._has_state(TaskState.PREDICTED_MASK):
|
if waiting_spiff_task._has_state(TaskState.PREDICTED_MASK):
|
||||||
@ -267,17 +268,21 @@ class RunUntilServiceTaskExecutionStrategy(ExecutionStrategy):
|
|||||||
|
|
||||||
class RunUntilUserTaskOrMessageExecutionStrategy(ExecutionStrategy):
|
class RunUntilUserTaskOrMessageExecutionStrategy(ExecutionStrategy):
|
||||||
"""When you want to run tasks until you hit something to report to the end user, or
|
"""When you want to run tasks until you hit something to report to the end user, or
|
||||||
until there are no other engine steps to complete."""
|
until there are no other engine steps to complete."""
|
||||||
|
|
||||||
def get_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> List[SpiffTask]:
|
def get_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> List[SpiffTask]:
|
||||||
return list([t for t in bpmn_process_instance.get_tasks(TaskState.READY) \
|
return list(
|
||||||
if t.task_spec.spec_type not in ["User Task", "Manual Task"] and
|
[
|
||||||
not (hasattr(t.task_spec, "extensions") and
|
t
|
||||||
t.task_spec.extensions.get("instructionsForEndUser", None))
|
for t in bpmn_process_instance.get_tasks(TaskState.READY)
|
||||||
])
|
if t.task_spec.spec_type not in ["User Task", "Manual Task"]
|
||||||
|
and not (
|
||||||
|
hasattr(t.task_spec, "extensions") and t.task_spec.extensions.get("instructionsForEndUser", None)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def spiff_run(self, bpmn_process_instance: BpmnWorkflow, exit_at: None = None) -> None:
|
def spiff_run(self, bpmn_process_instance: BpmnWorkflow, exit_at: None = None) -> None:
|
||||||
|
|
||||||
engine_steps = self.get_engine_steps(bpmn_process_instance)
|
engine_steps = self.get_engine_steps(bpmn_process_instance)
|
||||||
while engine_steps:
|
while engine_steps:
|
||||||
for task in engine_steps:
|
for task in engine_steps:
|
||||||
@ -320,12 +325,12 @@ class WorkflowExecutionService:
|
|||||||
"""Provides the driver code for workflow execution."""
|
"""Provides the driver code for workflow execution."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
bpmn_process_instance: BpmnWorkflow,
|
bpmn_process_instance: BpmnWorkflow,
|
||||||
process_instance_model: ProcessInstanceModel,
|
process_instance_model: ProcessInstanceModel,
|
||||||
execution_strategy: ExecutionStrategy,
|
execution_strategy: ExecutionStrategy,
|
||||||
process_instance_completer: ProcessInstanceCompleter,
|
process_instance_completer: ProcessInstanceCompleter,
|
||||||
process_instance_saver: ProcessInstanceSaver,
|
process_instance_saver: ProcessInstanceSaver,
|
||||||
):
|
):
|
||||||
"""__init__."""
|
"""__init__."""
|
||||||
self.bpmn_process_instance = bpmn_process_instance
|
self.bpmn_process_instance = bpmn_process_instance
|
||||||
@ -402,12 +407,12 @@ class WorkflowExecutionService:
|
|||||||
for event in waiting_message_events:
|
for event in waiting_message_events:
|
||||||
# Ensure we are only creating one message instance for each waiting message
|
# Ensure we are only creating one message instance for each waiting message
|
||||||
if (
|
if (
|
||||||
MessageInstanceModel.query.filter_by(
|
MessageInstanceModel.query.filter_by(
|
||||||
process_instance_id=self.process_instance_model.id,
|
process_instance_id=self.process_instance_model.id,
|
||||||
message_type="receive",
|
message_type="receive",
|
||||||
name=event["name"],
|
name=event["name"],
|
||||||
).count()
|
).count()
|
||||||
> 0
|
> 0
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -120,4 +120,4 @@
|
|||||||
</bpmndi:BPMNEdge>
|
</bpmndi:BPMNEdge>
|
||||||
</bpmndi:BPMNPlane>
|
</bpmndi:BPMNPlane>
|
||||||
</bpmndi:BPMNDiagram>
|
</bpmndi:BPMNDiagram>
|
||||||
</bpmn:definitions>
|
</bpmn:definitions>
|
||||||
|
@ -3,14 +3,13 @@ from typing import Any
|
|||||||
|
|
||||||
from flask.app import Flask
|
from flask.app import Flask
|
||||||
from flask.testing import FlaskClient
|
from flask.testing import FlaskClient
|
||||||
|
|
||||||
from spiffworkflow_backend.routes.tasks_controller import _interstitial_stream
|
|
||||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||||
|
|
||||||
from spiffworkflow_backend import db
|
from spiffworkflow_backend import db
|
||||||
from spiffworkflow_backend.models.human_task import HumanTaskModel
|
from spiffworkflow_backend.models.human_task import HumanTaskModel
|
||||||
from spiffworkflow_backend.models.user import UserModel
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
|
from spiffworkflow_backend.routes.tasks_controller import _interstitial_stream
|
||||||
|
|
||||||
|
|
||||||
class TestForGoodErrors(BaseTest):
|
class TestForGoodErrors(BaseTest):
|
||||||
@ -22,7 +21,6 @@ class TestForGoodErrors(BaseTest):
|
|||||||
client: FlaskClient,
|
client: FlaskClient,
|
||||||
with_super_admin_user: UserModel,
|
with_super_admin_user: UserModel,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
|
|
||||||
# Call this to assure all engine-steps are fully processed before we search for human tasks.
|
# Call this to assure all engine-steps are fully processed before we search for human tasks.
|
||||||
_interstitial_stream(process_instance_id)
|
_interstitial_stream(process_instance_id)
|
||||||
|
|
||||||
|
@ -10,8 +10,6 @@ import pytest
|
|||||||
from flask.app import Flask
|
from flask.app import Flask
|
||||||
from flask.testing import FlaskClient
|
from flask.testing import FlaskClient
|
||||||
from SpiffWorkflow.task import TaskState # type: ignore
|
from SpiffWorkflow.task import TaskState # type: ignore
|
||||||
|
|
||||||
from spiffworkflow_backend.routes.tasks_controller import _interstitial_stream
|
|
||||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||||
|
|
||||||
@ -35,6 +33,7 @@ from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema
|
|||||||
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
|
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
|
||||||
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
||||||
from spiffworkflow_backend.models.user import UserModel
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
|
from spiffworkflow_backend.routes.tasks_controller import _interstitial_stream
|
||||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
||||||
from spiffworkflow_backend.services.process_instance_processor import (
|
from spiffworkflow_backend.services.process_instance_processor import (
|
||||||
@ -1615,13 +1614,12 @@ class TestProcessApi(BaseTest):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def test_interstitial_page(
|
def test_interstitial_page(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
client: FlaskClient,
|
client: FlaskClient,
|
||||||
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:
|
||||||
|
|
||||||
process_group_id = "my_process_group"
|
process_group_id = "my_process_group"
|
||||||
process_model_id = "interstitial"
|
process_model_id = "interstitial"
|
||||||
bpmn_file_location = "interstitial"
|
bpmn_file_location = "interstitial"
|
||||||
@ -1649,23 +1647,24 @@ class TestProcessApi(BaseTest):
|
|||||||
|
|
||||||
assert response.json is not None
|
assert response.json is not None
|
||||||
assert response.json["next_task"] is not None
|
assert response.json["next_task"] is not None
|
||||||
assert response.json["next_task"]["state"] == 'READY'
|
assert response.json["next_task"]["state"] == "READY"
|
||||||
assert response.json["next_task"]["title"] == 'Script Task #2'
|
assert response.json["next_task"]["title"] == "Script Task #2"
|
||||||
|
|
||||||
# Rather that call the API and deal with the Server Side Events, call the loop directly and covert it to
|
# Rather that call the API and deal with the Server Side Events, call the loop directly and covert it to
|
||||||
# a list. It tests all of our code. No reason to test Flasks SSE support.
|
# a list. It tests all of our code. No reason to test Flasks SSE support.
|
||||||
results = list(_interstitial_stream(process_instance_id))
|
results = list(_interstitial_stream(process_instance_id))
|
||||||
json_results = list(map(lambda x: json.loads(x[5:]), results)) # strip the "data:" prefix and convert remaining string to dict.
|
# strip the "data:" prefix and convert remaining string to dict.
|
||||||
|
json_results = list(map(lambda x: json.loads(x[5:]), results))
|
||||||
# There should be 2 results back -
|
# There should be 2 results back -
|
||||||
# the first script task should not be returned (it contains no end user instructions)
|
# the first script task should not be returned (it contains no end user instructions)
|
||||||
# The second script task should produce rendered jinja text
|
# The second script task should produce rendered jinja text
|
||||||
# The Manual Task should then return a message as well.
|
# The Manual Task should then return a message as well.
|
||||||
assert len(results) == 2
|
assert len(results) == 2
|
||||||
assert json_results[0]["state"] == 'READY'
|
assert json_results[0]["state"] == "READY"
|
||||||
assert json_results[0]["title"] == 'Script Task #2'
|
assert json_results[0]["title"] == "Script Task #2"
|
||||||
assert json_results[0]["properties"]["instructionsForEndUser"] == 'I am Script Task 2'
|
assert json_results[0]["properties"]["instructionsForEndUser"] == "I am Script Task 2"
|
||||||
assert json_results[1]["state"] == 'READY'
|
assert json_results[1]["state"] == "READY"
|
||||||
assert json_results[1]["title"] == 'Manual Task'
|
assert json_results[1]["title"] == "Manual Task"
|
||||||
|
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/v1.0/tasks/{process_instance_id}/{json_results[1]['id']}",
|
f"/v1.0/tasks/{process_instance_id}/{json_results[1]['id']}",
|
||||||
@ -1678,10 +1677,10 @@ class TestProcessApi(BaseTest):
|
|||||||
results = list(_interstitial_stream(process_instance_id))
|
results = list(_interstitial_stream(process_instance_id))
|
||||||
json_results = list(map(lambda x: json.loads(x[5:]), results))
|
json_results = list(map(lambda x: json.loads(x[5:]), results))
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
assert json_results[0]["state"] == 'READY'
|
assert json_results[0]["state"] == "READY"
|
||||||
assert json_results[0]["can_complete"] == False
|
assert json_results[0]["can_complete"] == False
|
||||||
assert json_results[0]["title"] == 'Please Approve'
|
assert json_results[0]["title"] == "Please Approve"
|
||||||
assert json_results[0]["properties"]["instructionsForEndUser"] == 'I am a manual task in another lane'
|
assert json_results[0]["properties"]["instructionsForEndUser"] == "I am a manual task in another lane"
|
||||||
|
|
||||||
# Complete task as the finance user.
|
# Complete task as the finance user.
|
||||||
response = client.put(
|
response = client.put(
|
||||||
@ -1690,13 +1689,13 @@ class TestProcessApi(BaseTest):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# We should now be on the end task with a valid message, even after loading it many times.
|
# We should now be on the end task with a valid message, even after loading it many times.
|
||||||
results_1 = list(_interstitial_stream(process_instance_id))
|
list(_interstitial_stream(process_instance_id))
|
||||||
results_2 = list(_interstitial_stream(process_instance_id))
|
list(_interstitial_stream(process_instance_id))
|
||||||
results = list(_interstitial_stream(process_instance_id))
|
results = list(_interstitial_stream(process_instance_id))
|
||||||
json_results = list(map(lambda x: json.loads(x[5:]), results))
|
json_results = list(map(lambda x: json.loads(x[5:]), results))
|
||||||
assert len(json_results) == 1
|
assert len(json_results) == 1
|
||||||
assert json_results[0]["state"] == 'COMPLETED'
|
assert json_results[0]["state"] == "COMPLETED"
|
||||||
assert json_results[0]["properties"]["instructionsForEndUser"] == 'I am the end task'
|
assert json_results[0]["properties"]["instructionsForEndUser"] == "I am the end task"
|
||||||
|
|
||||||
def test_process_instance_list_with_default_list(
|
def test_process_instance_list_with_default_list(
|
||||||
self,
|
self,
|
||||||
|
@ -405,7 +405,7 @@ class TestProcessInstanceProcessor(BaseTest):
|
|||||||
process_model=process_model, user=initiator_user
|
process_model=process_model, user=initiator_user
|
||||||
)
|
)
|
||||||
processor = ProcessInstanceProcessor(process_instance)
|
processor = ProcessInstanceProcessor(process_instance)
|
||||||
processor.do_engine_steps(save=True)
|
processor.do_engine_steps(save=True, execution_strategy_name="greedy")
|
||||||
assert len(process_instance.active_human_tasks) == 1
|
assert len(process_instance.active_human_tasks) == 1
|
||||||
initial_human_task_id = process_instance.active_human_tasks[0].id
|
initial_human_task_id = process_instance.active_human_tasks[0].id
|
||||||
|
|
||||||
@ -436,7 +436,8 @@ class TestProcessInstanceProcessor(BaseTest):
|
|||||||
# recreate variables to ensure all bpmn json was recreated from scratch from the db
|
# recreate variables to ensure all bpmn json was recreated from scratch from the db
|
||||||
process_instance_relookup = ProcessInstanceModel.query.filter_by(id=process_instance.id).first()
|
process_instance_relookup = ProcessInstanceModel.query.filter_by(id=process_instance.id).first()
|
||||||
processor_final = ProcessInstanceProcessor(process_instance_relookup)
|
processor_final = ProcessInstanceProcessor(process_instance_relookup)
|
||||||
processor.do_engine_steps(save=True, execution_strategy_name="greedy")
|
processor_final.do_engine_steps(save=True, execution_strategy_name="greedy")
|
||||||
|
|
||||||
assert process_instance_relookup.status == "complete"
|
assert process_instance_relookup.status == "complete"
|
||||||
|
|
||||||
data_set_1 = {"set_in_top_level_script": 1}
|
data_set_1 = {"set_in_top_level_script": 1}
|
||||||
@ -548,7 +549,6 @@ class TestProcessInstanceProcessor(BaseTest):
|
|||||||
# assert task_model.python_env_data() == expected_python_env_data, message
|
# assert task_model.python_env_data() == expected_python_env_data, message
|
||||||
assert task_model.json_data() == expected_python_env_data, message
|
assert task_model.json_data() == expected_python_env_data, message
|
||||||
|
|
||||||
processor_final.do_engine_steps(save=True, execution_strategy_name="greedy")
|
|
||||||
all_spiff_tasks = processor_final.bpmn_process_instance.get_tasks()
|
all_spiff_tasks = processor_final.bpmn_process_instance.get_tasks()
|
||||||
assert len(all_spiff_tasks) > 1
|
assert len(all_spiff_tasks) > 1
|
||||||
for spiff_task in all_spiff_tasks:
|
for spiff_task in all_spiff_tasks:
|
||||||
@ -607,7 +607,7 @@ class TestProcessInstanceProcessor(BaseTest):
|
|||||||
)
|
)
|
||||||
assert task_models_that_are_predicted_count == 0
|
assert task_models_that_are_predicted_count == 0
|
||||||
|
|
||||||
assert processor.get_data() == data_set_7
|
assert processor_final.get_data() == data_set_7
|
||||||
|
|
||||||
def test_does_not_recreate_human_tasks_on_multiple_saves(
|
def test_does_not_recreate_human_tasks_on_multiple_saves(
|
||||||
self,
|
self,
|
||||||
|
20
spiffworkflow-frontend/package-lock.json
generated
20
spiffworkflow-frontend/package-lock.json
generated
@ -4477,11 +4477,6 @@
|
|||||||
"@lezer/common": "^1.0.0"
|
"@lezer/common": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@microsoft/fetch-event-source": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
|
|
||||||
},
|
|
||||||
"node_modules/@lezer/markdown": {
|
"node_modules/@lezer/markdown": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.0.2.tgz",
|
||||||
@ -4492,6 +4487,11 @@
|
|||||||
"@lezer/highlight": "^1.0.0"
|
"@lezer/highlight": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@microsoft/fetch-event-source": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
|
||||||
|
},
|
||||||
"node_modules/@monaco-editor/loader": {
|
"node_modules/@monaco-editor/loader": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.3.3.tgz",
|
||||||
@ -35359,11 +35359,6 @@
|
|||||||
"@lezer/common": "^1.0.0"
|
"@lezer/common": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@microsoft/fetch-event-source": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
|
|
||||||
},
|
|
||||||
"@lezer/markdown": {
|
"@lezer/markdown": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.0.2.tgz",
|
||||||
@ -35374,6 +35369,11 @@
|
|||||||
"@lezer/highlight": "^1.0.0"
|
"@lezer/highlight": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@microsoft/fetch-event-source": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
|
||||||
|
},
|
||||||
"@monaco-editor/loader": {
|
"@monaco-editor/loader": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.3.3.tgz",
|
||||||
|
@ -7,7 +7,7 @@ export default function InstructionsForEndUser({ task }: any) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let instructions = '';
|
let instructions = '';
|
||||||
console.log("I was passed a task: ", task);
|
console.log('I was passed a task: ', task);
|
||||||
const { properties } = task;
|
const { properties } = task;
|
||||||
const { instructionsForEndUser } = properties;
|
const { instructionsForEndUser } = properties;
|
||||||
if (instructionsForEndUser) {
|
if (instructionsForEndUser) {
|
||||||
|
@ -1455,7 +1455,8 @@ export default function ProcessInstanceListTable({
|
|||||||
if (showActionsColumn) {
|
if (showActionsColumn) {
|
||||||
let buttonElement = null;
|
let buttonElement = null;
|
||||||
const interstitialUrl = `/process/${modifyProcessIdentifierForPathParam(
|
const interstitialUrl = `/process/${modifyProcessIdentifierForPathParam(
|
||||||
row.process_model_identifier)}/${row.id}/interstitial`
|
row.process_model_identifier
|
||||||
|
)}/${row.id}/interstitial`;
|
||||||
const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`);
|
const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`);
|
||||||
let hasAccessToCompleteTask = false;
|
let hasAccessToCompleteTask = false;
|
||||||
if (
|
if (
|
||||||
|
@ -56,7 +56,10 @@ export default function HomePageRoutes() {
|
|||||||
<Route path="my-tasks" element={<MyTasks />} />
|
<Route path="my-tasks" element={<MyTasks />} />
|
||||||
<Route path=":process_instance_id/:task_id" element={<TaskShow />} />
|
<Route path=":process_instance_id/:task_id" element={<TaskShow />} />
|
||||||
<Route path="grouped" element={<InProgressInstances />} />
|
<Route path="grouped" element={<InProgressInstances />} />
|
||||||
<Route path="process/:process_instance_id/interstitial" element={<ProcessInterstitial />} />
|
<Route
|
||||||
|
path="process/:process_instance_id/interstitial"
|
||||||
|
element={<ProcessInterstitial />}
|
||||||
|
/>
|
||||||
<Route path="completed-instances" element={<CompletedInstances />} />
|
<Route path="completed-instances" element={<CompletedInstances />} />
|
||||||
<Route path="create-new-instance" element={<CreateNewInstance />} />
|
<Route path="create-new-instance" element={<CreateNewInstance />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
@ -75,7 +75,12 @@ export default function ProcessInterstitial() {
|
|||||||
case 'COMPLETED':
|
case 'COMPLETED':
|
||||||
return <img src="/interstitial/checkmark.png" alt="Completed" />;
|
return <img src="/interstitial/checkmark.png" alt="Completed" />;
|
||||||
case 'LOCKED':
|
case 'LOCKED':
|
||||||
return <img src="/interstitial/lock.png" alt="Locked, Waiting on someone else." />;
|
return (
|
||||||
|
<img
|
||||||
|
src="/interstitial/lock.png"
|
||||||
|
alt="Locked, Waiting on someone else."
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -86,11 +91,13 @@ export default function ProcessInterstitial() {
|
|||||||
!myTask.can_complete &&
|
!myTask.can_complete &&
|
||||||
['User Task', 'Manual Task'].includes(myTask.type)
|
['User Task', 'Manual Task'].includes(myTask.type)
|
||||||
) {
|
) {
|
||||||
return (
|
return <div>This next task must be completed by a different person.</div>;
|
||||||
<div>This next task must be completed by a different person.</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return <div><InstructionsForEndUser task={myTask} /></div>;
|
return (
|
||||||
|
<div>
|
||||||
|
<InstructionsForEndUser task={myTask} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (lastTask) {
|
if (lastTask) {
|
||||||
@ -116,7 +123,9 @@ export default function ProcessInterstitial() {
|
|||||||
<Column md={6} lg={8} sm={4}>
|
<Column md={6} lg={8} sm={4}>
|
||||||
{data &&
|
{data &&
|
||||||
data.map((d) => (
|
data.map((d) => (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '2em' }}>
|
<div
|
||||||
|
style={{ display: 'flex', alignItems: 'center', gap: '2em' }}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
Task: <em>{d.title}</em>
|
Task: <em>{d.title}</em>
|
||||||
</div>
|
</div>
|
||||||
|
@ -126,7 +126,6 @@ export default function TaskShow() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const processResult = (result: ProcessInstanceTask) => {
|
const processResult = (result: ProcessInstanceTask) => {
|
||||||
setTask(result);
|
setTask(result);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user