Add a test for the interstitial endpoint.
Assure all "GO" buttons lead to the interstial page, and display differently depending on if there is anything you can actually do.
This commit is contained in:
parent
5297e4c9cc
commit
be14b9c05f
|
@ -140,7 +140,7 @@ SPIFFWORKFLOW_BACKEND_ENGINE_STEP_DEFAULT_STRATEGY_BACKGROUND = environ.get(
|
||||||
)
|
)
|
||||||
|
|
||||||
SPIFFWORKFLOW_BACKEND_ENGINE_STEP_DEFAULT_STRATEGY_WEB = environ.get(
|
SPIFFWORKFLOW_BACKEND_ENGINE_STEP_DEFAULT_STRATEGY_WEB = environ.get(
|
||||||
"SPIFFWORKFLOW_BACKEND_ENGINE_STEP_DEFAULT_STRATEGY_WEB", default="greedy"
|
"SPIFFWORKFLOW_BACKEND_ENGINE_STEP_DEFAULT_STRATEGY_WEB", default="run_until_user_message"
|
||||||
)
|
)
|
||||||
|
|
||||||
# this is only used in CI. use SPIFFWORKFLOW_BACKEND_DATABASE_URI instead for real configuration
|
# this is only used in CI. use SPIFFWORKFLOW_BACKEND_DATABASE_URI instead for real configuration
|
||||||
|
|
|
@ -85,7 +85,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 +166,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 +178,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 +188,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 +241,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)
|
||||||
|
@ -353,12 +353,13 @@ def task_show(process_instance_id: int, task_guid: str = "next") -> flask.wrappe
|
||||||
_render_instructions_for_end_user(spiff_task, task)
|
_render_instructions_for_end_user(spiff_task, task)
|
||||||
return make_response(jsonify(task), 200)
|
return make_response(jsonify(task), 200)
|
||||||
|
|
||||||
|
|
||||||
def _render_instructions_for_end_user(spiff_task: SpiffTask, task: Task):
|
def _render_instructions_for_end_user(spiff_task: SpiffTask, task: Task):
|
||||||
"""Assure any instructions for end user are processed for jinja syntax."""
|
"""Assure any instructions for end user are processed for jinja syntax."""
|
||||||
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
|
||||||
|
@ -370,9 +371,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)
|
||||||
|
@ -392,36 +393,42 @@ def process_data_show(
|
||||||
200,
|
200,
|
||||||
)
|
)
|
||||||
|
|
||||||
def interstitial(process_instance_id: int):
|
|
||||||
|
def _interstitial_stream(process_instance_id: int):
|
||||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||||
processor = ProcessInstanceProcessor(process_instance)
|
processor = ProcessInstanceProcessor(process_instance)
|
||||||
reported_ids = [] # bit of an issue with end tasks showing as getting completed twice.
|
reported_ids = [] # bit of an issue with end tasks showing as getting completed twice.
|
||||||
def get_data():
|
|
||||||
spiff_task = processor.next_task()
|
|
||||||
last_task = None
|
|
||||||
while last_task != spiff_task:
|
|
||||||
task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
|
|
||||||
instructions = _render_instructions_for_end_user(spiff_task, task)
|
|
||||||
if instructions and spiff_task.id not in reported_ids:
|
|
||||||
reported_ids.append(spiff_task.id)
|
|
||||||
yield f'data: {current_app.json.dumps(task)} \n\n'
|
|
||||||
last_task = spiff_task
|
|
||||||
processor.do_engine_steps(execution_strategy_name="run_until_user_message")
|
|
||||||
processor.do_engine_steps(execution_strategy_name="one_at_a_time")
|
|
||||||
spiff_task = processor.next_task()
|
|
||||||
# Note, this has to be done in case someone leaves the page,
|
|
||||||
# which can otherwise cancel this function and leave completed tasks un-registered.
|
|
||||||
processor.save() # Fixme - maybe find a way not to do this on every method?
|
|
||||||
return
|
|
||||||
|
|
||||||
# return Response(get_data(), mimetype='text/event-stream')
|
# return Response(get_data(), mimetype='text/event-stream')
|
||||||
return Response(stream_with_context(get_data()), mimetype='text/event-stream')
|
|
||||||
|
spiff_task = processor.next_task()
|
||||||
|
last_task = None
|
||||||
|
while last_task != spiff_task:
|
||||||
|
task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
|
||||||
|
instructions = _render_instructions_for_end_user(spiff_task, task)
|
||||||
|
if instructions and spiff_task.id not in reported_ids:
|
||||||
|
reported_ids.append(spiff_task.id)
|
||||||
|
yield f'data: {current_app.json.dumps(task)} \n\n'
|
||||||
|
last_task = spiff_task
|
||||||
|
processor.do_engine_steps(execution_strategy_name="run_until_user_message")
|
||||||
|
processor.do_engine_steps(execution_strategy_name="one_at_a_time")
|
||||||
|
spiff_task = processor.next_task()
|
||||||
|
# Note, this has to be done in case someone leaves the page,
|
||||||
|
# which can otherwise cancel this function and leave completed tasks un-registered.
|
||||||
|
processor.save() # Fixme - maybe find a way not to do this on every method?
|
||||||
|
|
||||||
|
|
||||||
|
def interstitial(process_instance_id: int):
|
||||||
|
"""A Server Side Events Stream for watching the execution of engine tasks in a
|
||||||
|
process instance. """
|
||||||
|
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)
|
||||||
|
@ -508,11 +515,12 @@ def _task_submit_shared(
|
||||||
"process_instance_id": process_instance_id
|
"process_instance_id": process_instance_id
|
||||||
}), status=202, mimetype="application/json")
|
}), 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"):
|
||||||
|
@ -520,11 +528,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
|
||||||
|
@ -671,9 +679,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:
|
||||||
|
@ -729,9 +737,8 @@ 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 {
|
||||||
|
@ -769,9 +776,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(
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
<?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:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" 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:collaboration id="Collaboration_1ullb3f">
|
||||||
|
<bpmn:participant id="Participant_1mug4yn" processRef="Process_a6ss9w7" />
|
||||||
|
</bpmn:collaboration>
|
||||||
|
<bpmn:process id="Process_a6ss9w7" isExecutable="true">
|
||||||
|
<bpmn:laneSet id="LaneSet_1m2geb1">
|
||||||
|
<bpmn:lane id="Lane_0518vyo">
|
||||||
|
<bpmn:flowNodeRef>StartEvent_1</bpmn:flowNodeRef>
|
||||||
|
<bpmn:flowNodeRef>Activity_16m8jvv</bpmn:flowNodeRef>
|
||||||
|
<bpmn:flowNodeRef>Activity_1qrme8m</bpmn:flowNodeRef>
|
||||||
|
<bpmn:flowNodeRef>Activity_0bi0v5d</bpmn:flowNodeRef>
|
||||||
|
<bpmn:flowNodeRef>Event_1vyxv42</bpmn:flowNodeRef>
|
||||||
|
</bpmn:lane>
|
||||||
|
<bpmn:lane id="Lane_0mx423x" name="Finance Team">
|
||||||
|
<bpmn:flowNodeRef>Activity_02ldrj6</bpmn:flowNodeRef>
|
||||||
|
</bpmn:lane>
|
||||||
|
</bpmn:laneSet>
|
||||||
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
|
<bpmn:outgoing>Flow_1rnrr8l</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1rnrr8l" sourceRef="StartEvent_1" targetRef="Activity_16m8jvv" />
|
||||||
|
<bpmn:sequenceFlow id="Flow_011ysja" sourceRef="Activity_16m8jvv" targetRef="Activity_1qrme8m" />
|
||||||
|
<bpmn:scriptTask id="Activity_16m8jvv" name="Script Task #1">
|
||||||
|
<bpmn:extensionElements>
|
||||||
|
<spiffworkflow:instructionsForEndUser />
|
||||||
|
</bpmn:extensionElements>
|
||||||
|
<bpmn:incoming>Flow_1rnrr8l</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_011ysja</bpmn:outgoing>
|
||||||
|
<bpmn:script>x = 2</bpmn:script>
|
||||||
|
</bpmn:scriptTask>
|
||||||
|
<bpmn:scriptTask id="Activity_1qrme8m" name="Script Task #2">
|
||||||
|
<bpmn:extensionElements>
|
||||||
|
<spiffworkflow:instructionsForEndUser>I am Script Task {{x}}</spiffworkflow:instructionsForEndUser>
|
||||||
|
</bpmn:extensionElements>
|
||||||
|
<bpmn:incoming>Flow_011ysja</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_1rab9xv</bpmn:outgoing>
|
||||||
|
<bpmn:script>x = 2</bpmn:script>
|
||||||
|
</bpmn:scriptTask>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1rab9xv" sourceRef="Activity_1qrme8m" targetRef="Activity_0bi0v5d" />
|
||||||
|
<bpmn:manualTask id="Activity_0bi0v5d" name="Manual Task">
|
||||||
|
<bpmn:extensionElements>
|
||||||
|
<spiffworkflow:instructionsForEndUser>I am a manual task</spiffworkflow:instructionsForEndUser>
|
||||||
|
</bpmn:extensionElements>
|
||||||
|
<bpmn:incoming>Flow_1rab9xv</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_1icul0s</bpmn:outgoing>
|
||||||
|
</bpmn:manualTask>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1icul0s" sourceRef="Activity_0bi0v5d" targetRef="Activity_02ldrj6" />
|
||||||
|
<bpmn:manualTask id="Activity_02ldrj6" name="Please Approve">
|
||||||
|
<bpmn:extensionElements>
|
||||||
|
<spiffworkflow:instructionsForEndUser>I am a manual task in another lane</spiffworkflow:instructionsForEndUser>
|
||||||
|
</bpmn:extensionElements>
|
||||||
|
<bpmn:incoming>Flow_1icul0s</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_06qy6r3</bpmn:outgoing>
|
||||||
|
</bpmn:manualTask>
|
||||||
|
<bpmn:sequenceFlow id="Flow_06qy6r3" sourceRef="Activity_02ldrj6" targetRef="Event_1vyxv42" />
|
||||||
|
<bpmn:endEvent id="Event_1vyxv42">
|
||||||
|
<bpmn:extensionElements>
|
||||||
|
<spiffworkflow:instructionsForEndUser>I am the end task</spiffworkflow:instructionsForEndUser>
|
||||||
|
</bpmn:extensionElements>
|
||||||
|
<bpmn:incoming>Flow_06qy6r3</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_1ullb3f">
|
||||||
|
<bpmndi:BPMNShape id="Participant_1mug4yn_di" bpmnElement="Participant_1mug4yn" isHorizontal="true">
|
||||||
|
<dc:Bounds x="129" y="130" width="971" height="250" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Lane_0mx423x_di" bpmnElement="Lane_0mx423x" isHorizontal="true">
|
||||||
|
<dc:Bounds x="159" y="255" width="941" height="125" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Lane_0518vyo_di" bpmnElement="Lane_0518vyo" isHorizontal="true">
|
||||||
|
<dc:Bounds x="159" y="130" width="941" height="125" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||||
|
<dc:Bounds x="179" y="172" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0lp6dyb_di" bpmnElement="Activity_16m8jvv">
|
||||||
|
<dc:Bounds x="270" y="150" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_1dlfog4_di" bpmnElement="Activity_1qrme8m">
|
||||||
|
<dc:Bounds x="430" y="150" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0bpymtg_di" bpmnElement="Activity_0bi0v5d">
|
||||||
|
<dc:Bounds x="580" y="150" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_06oz8dg_di" bpmnElement="Activity_02ldrj6">
|
||||||
|
<dc:Bounds x="730" y="280" width="100" height="80" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_1vyxv42_di" bpmnElement="Event_1vyxv42">
|
||||||
|
<dc:Bounds x="872" y="172" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1rnrr8l_di" bpmnElement="Flow_1rnrr8l">
|
||||||
|
<di:waypoint x="215" y="190" />
|
||||||
|
<di:waypoint x="270" y="190" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_011ysja_di" bpmnElement="Flow_011ysja">
|
||||||
|
<di:waypoint x="370" y="190" />
|
||||||
|
<di:waypoint x="430" y="190" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1rab9xv_di" bpmnElement="Flow_1rab9xv">
|
||||||
|
<di:waypoint x="530" y="190" />
|
||||||
|
<di:waypoint x="580" y="190" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1icul0s_di" bpmnElement="Flow_1icul0s">
|
||||||
|
<di:waypoint x="680" y="190" />
|
||||||
|
<di:waypoint x="705" y="190" />
|
||||||
|
<di:waypoint x="705" y="320" />
|
||||||
|
<di:waypoint x="730" y="320" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_06qy6r3_di" bpmnElement="Flow_06qy6r3">
|
||||||
|
<di:waypoint x="830" y="320" />
|
||||||
|
<di:waypoint x="851" y="320" />
|
||||||
|
<di:waypoint x="851" y="190" />
|
||||||
|
<di:waypoint x="872" y="190" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
|
@ -10,6 +10,8 @@ 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
|
||||||
|
|
||||||
|
@ -1611,6 +1613,90 @@ class TestProcessApi(BaseTest):
|
||||||
"veryImportantFieldButOnlySometimes": {"ui:widget": "hidden"},
|
"veryImportantFieldButOnlySometimes": {"ui:widget": "hidden"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_interstitial_page(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
process_group_id = "my_process_group"
|
||||||
|
process_model_id = "interstitial"
|
||||||
|
bpmn_file_location = "interstitial"
|
||||||
|
# Assure we have someone in the finance team
|
||||||
|
finance_user = self.find_or_create_user("testuser2")
|
||||||
|
AuthorizationService.import_permissions_from_yaml_file()
|
||||||
|
process_model_identifier = self.create_group_and_model_with_bpmn(
|
||||||
|
client,
|
||||||
|
with_super_admin_user,
|
||||||
|
process_group_id=process_group_id,
|
||||||
|
process_model_id=process_model_id,
|
||||||
|
bpmn_file_location=bpmn_file_location,
|
||||||
|
)
|
||||||
|
headers = self.logged_in_headers(with_super_admin_user)
|
||||||
|
response = self.create_process_instance_from_process_model_id_with_api(
|
||||||
|
client, process_model_identifier, headers
|
||||||
|
)
|
||||||
|
assert response.json is not None
|
||||||
|
process_instance_id = response.json["id"]
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.json is not None
|
||||||
|
assert response.json["next_task"] is not None
|
||||||
|
assert response.json["next_task"]["state"] == 'READY'
|
||||||
|
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
|
||||||
|
# a list. It tests all of our code. No reason to test Flasks SSE support.
|
||||||
|
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.
|
||||||
|
# There should be 2 results back -
|
||||||
|
# the first script task should not be returned (it contains no end user instructions)
|
||||||
|
# The second script task should produce rendered jinja text
|
||||||
|
# The Manual Task should then return a message as well.
|
||||||
|
assert len(results) == 2
|
||||||
|
assert json_results[0]["state"] == 'READY'
|
||||||
|
assert json_results[0]["title"] == 'Script Task #2'
|
||||||
|
assert json_results[0]["properties"]["instructionsForEndUser"] == 'I am Script Task 2'
|
||||||
|
assert json_results[1]["state"] == 'READY'
|
||||||
|
assert json_results[1]["title"] == 'Manual Task'
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/v1.0/tasks/{process_instance_id}/{json_results[1]['id']}",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.json is not None
|
||||||
|
|
||||||
|
# we should now be on a task that does not belong to the original user, and the interstitial page should know this.
|
||||||
|
results = list(_interstitial_stream(process_instance_id))
|
||||||
|
json_results = list(map(lambda x: json.loads(x[5:]), results))
|
||||||
|
assert len(results) == 1
|
||||||
|
assert json_results[0]["state"] == 'READY'
|
||||||
|
assert json_results[0]["can_complete"] == False
|
||||||
|
assert json_results[0]["title"] == 'Please Approve'
|
||||||
|
assert json_results[0]["properties"]["instructionsForEndUser"] == 'I am a manual task in another lane'
|
||||||
|
|
||||||
|
# Complete task as the finance user.
|
||||||
|
response = client.put(
|
||||||
|
f"/v1.0/tasks/{process_instance_id}/{json_results[0]['id']}",
|
||||||
|
headers=self.logged_in_headers(finance_user),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
results_2 = list(_interstitial_stream(process_instance_id))
|
||||||
|
results = list(_interstitial_stream(process_instance_id))
|
||||||
|
json_results = list(map(lambda x: json.loads(x[5:]), results))
|
||||||
|
assert len(json_results) == 1
|
||||||
|
assert json_results[0]["state"] == 'COMPLETED'
|
||||||
|
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,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
|
|
|
@ -8072,7 +8072,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/bpmn-js-spiffworkflow": {
|
"node_modules/bpmn-js-spiffworkflow": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#69135655f8a5282bcdaef82705c3d522ef5b4464",
|
"resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#2214ac6432dec77cb2d1362615e6320bfea7df1f",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.4",
|
"inherits": "^2.0.4",
|
||||||
|
@ -38238,7 +38238,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bpmn-js-spiffworkflow": {
|
"bpmn-js-spiffworkflow": {
|
||||||
"version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#69135655f8a5282bcdaef82705c3d522ef5b4464",
|
"version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#2214ac6432dec77cb2d1362615e6320bfea7df1f",
|
||||||
"from": "bpmn-js-spiffworkflow@sartography/bpmn-js-spiffworkflow#main",
|
"from": "bpmn-js-spiffworkflow@sartography/bpmn-js-spiffworkflow#main",
|
||||||
"requires": {
|
"requires": {
|
||||||
"inherits": "^2.0.4",
|
"inherits": "^2.0.4",
|
||||||
|
|
|
@ -1443,28 +1443,27 @@ export default function ProcessInstanceListTable({
|
||||||
});
|
});
|
||||||
if (showActionsColumn) {
|
if (showActionsColumn) {
|
||||||
let buttonElement = null;
|
let buttonElement = null;
|
||||||
if (row.task_id) {
|
const interstitialUrl = `/process/${modifyProcessIdentifierForPathParam(
|
||||||
const taskUrl = `/tasks/${row.id}/${row.task_id}`;
|
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 (
|
||||||
canCompleteAllTasks ||
|
canCompleteAllTasks ||
|
||||||
(row.potential_owner_usernames || '').match(regex)
|
(row.potential_owner_usernames || '').match(regex)
|
||||||
) {
|
) {
|
||||||
hasAccessToCompleteTask = true;
|
hasAccessToCompleteTask = true;
|
||||||
}
|
|
||||||
buttonElement = (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
href={taskUrl}
|
|
||||||
hidden={row.status === 'suspended'}
|
|
||||||
disabled={!hasAccessToCompleteTask}
|
|
||||||
>
|
|
||||||
Go
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buttonElement = (
|
||||||
|
<Button
|
||||||
|
kind={
|
||||||
|
hasAccessToCompleteTask && row.task_id ? 'secondary' : 'tertiary'
|
||||||
|
}
|
||||||
|
href={interstitialUrl}
|
||||||
|
>
|
||||||
|
Go
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
currentRow.push(<td>{buttonElement}</td>);
|
currentRow.push(<td>{buttonElement}</td>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,10 +87,10 @@ export default function ProcessInterstitial() {
|
||||||
['User Task', 'Manual Task'].includes(myTask.type)
|
['User Task', 'Manual Task'].includes(myTask.type)
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div>This task is assigned to another user or group to complete. </div>
|
<div>This next task must be completed by a different person.</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <InstructionsForEndUser task={myTask} />;
|
return <div><InstructionsForEndUser task={myTask} /></div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (lastTask) {
|
if (lastTask) {
|
||||||
|
@ -114,21 +114,15 @@ export default function ProcessInterstitial() {
|
||||||
|
|
||||||
<Grid condensed fullWidth>
|
<Grid condensed fullWidth>
|
||||||
<Column md={6} lg={8} sm={4}>
|
<Column md={6} lg={8} sm={4}>
|
||||||
<table className="table table-bordered">
|
{data &&
|
||||||
<tbody>
|
data.map((d) => (
|
||||||
{data &&
|
<div style={{ display: 'flex', alignItems: 'center', gap: '2em' }}>
|
||||||
data.map((d) => (
|
<div>
|
||||||
<tr key={d.id}>
|
Task: <em>{d.title}</em>
|
||||||
<td>
|
</div>
|
||||||
<h3>{d.title}</h3>
|
<div>{userMessage(d)}</div>
|
||||||
</td>
|
</div>
|
||||||
<td>
|
))}
|
||||||
<p>{userMessage(d)}</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</Column>
|
</Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
</>
|
||||||
|
|
Loading…
Reference in New Issue