Revert "Revert "some updates so task_show no longer needs the processor. i think it mostly works w/ burnettk""

This reverts commit f4af53f1dd.
This commit is contained in:
jasquat 2023-04-21 16:34:05 -04:00
parent f4af53f1dd
commit ddf8283c71
17 changed files with 392 additions and 118 deletions

View File

@ -69,6 +69,17 @@ class TaskModel(SpiffworkflowBaseDBModel):
data: Optional[dict] = None
# these are here to be compatible with task api
form_schema: Optional[dict] = None
form_ui_schema: Optional[dict] = None
process_model_display_name: Optional[str] = None
process_model_identifier: Optional[str] = None
type: Optional[str] = None
can_complete: Optional[bool] = None
def get_data(self) -> dict:
return {**self.python_env_data(), **self.json_data()}
def python_env_data(self) -> dict:
return JsonDataModel.find_data_dict_by_hash(self.python_env_data_hash)

View File

@ -1,5 +1,7 @@
"""APIs for dealing with process groups, process models, and process instances."""
import json
from spiffworkflow_backend.services.authorization_service import UserDoesNotHaveAccessToTaskError
from spiffworkflow_backend.services.authorization_service import HumanTaskNotFoundError
import os
import uuid
from sys import exc_info
@ -283,39 +285,67 @@ def task_show(process_instance_id: int, task_guid: str = "next") -> flask.wrappe
form_schema_file_name = ""
form_ui_schema_file_name = ""
processor = ProcessInstanceProcessor(process_instance)
if task_guid == "next":
spiff_task = processor.next_task()
task_guid = spiff_task.id
else:
spiff_task = _get_spiff_task_from_process_instance(task_guid, process_instance, processor=processor)
spiff_task = _get_spiff_task_from_process_instance(task_guid, process_instance, processor=processor)
extensions = spiff_task.task_spec.extensions
task_model = _get_task_model_from_guid_or_raise(task_guid, process_instance_id)
# extensions = task_model.properties_json['extensions'] if 'extensions' in task_model.properties_json else {}
if "properties" in extensions:
properties = extensions["properties"]
if "formJsonSchemaFilename" in properties:
form_schema_file_name = properties["formJsonSchemaFilename"]
if "formUiSchemaFilename" in properties:
form_ui_schema_file_name = properties["formUiSchemaFilename"]
can_complete = False
try:
AuthorizationService.assert_user_can_complete_task(
process_instance.id, task_model.task_definition.bpmn_identifier, g.user
)
can_complete = True
except HumanTaskNotFoundError:
can_complete = False
except UserDoesNotHaveAccessToTaskError:
can_complete = False
task = ProcessInstanceService.spiff_task_to_api_task(processor, spiff_task)
task.data = spiff_task.data
task.process_model_display_name = process_model.display_name
task.process_model_identifier = process_model.id
# task.data
# task.form_schema
# task.form_ui_schema
# task.id
# task.process_model_display_name
# task.process_model_identifier
# task.state
# task.type
task_model.data = task_model.get_data()
task_model.process_model_display_name = process_model.display_name
task_model.process_model_identifier = process_model.id
task_model.type = task_model.task_definition.typename
task_model.can_complete = can_complete
task_process_identifier = task_model.bpmn_process.bpmn_process_definition.bpmn_identifier
process_model_with_form = process_model
refs = SpecFileService.get_references_for_process(process_model_with_form)
all_processes = [i.identifier for i in refs]
if task.process_identifier not in all_processes:
top_process_name = processor.find_process_model_process_name_by_task_name(task.process_identifier)
if task_process_identifier not in all_processes:
top_bpmn_process = TaskService.bpmn_process_for_called_activity_or_top_level_process(task_model)
bpmn_file_full_path = ProcessInstanceProcessor.bpmn_file_full_path_from_bpmn_process_identifier(
top_process_name
top_bpmn_process.bpmn_process_definition.bpmn_identifier
)
relative_path = os.path.relpath(bpmn_file_full_path, start=FileSystemService.root_path())
process_model_relative_path = os.path.dirname(relative_path)
process_model_with_form = ProcessModelService.get_process_model_from_relative_path(process_model_relative_path)
if task.type == "User Task":
if task_model.task_definition.typename == "UserTask":
if not form_schema_file_name:
raise (
ApiError(
@ -330,37 +360,38 @@ def task_show(process_instance_id: int, task_guid: str = "next") -> flask.wrappe
form_dict = _prepare_form_data(
form_schema_file_name,
spiff_task,
task_model,
process_model_with_form,
)
if task.data:
_update_form_schema_with_task_data_as_needed(form_dict, task, spiff_task)
if task_model.data:
_update_form_schema_with_task_data_as_needed(form_dict, task_model)
if form_dict:
task.form_schema = form_dict
task_model.form_schema = form_dict
if form_ui_schema_file_name:
ui_form_contents = _prepare_form_data(
form_ui_schema_file_name,
task,
task_model,
process_model_with_form,
)
if ui_form_contents:
task.form_ui_schema = ui_form_contents
task_model.form_ui_schema = ui_form_contents
_munge_form_ui_schema_based_on_hidden_fields_in_task_data(task)
_render_instructions_for_end_user(spiff_task, task)
return make_response(jsonify(task), 200)
_munge_form_ui_schema_based_on_hidden_fields_in_task_data(task_model)
_render_instructions_for_end_user(task_model)
return make_response(jsonify(task_model), 200)
def _render_instructions_for_end_user(spiff_task: SpiffTask, task: Task) -> str:
def _render_instructions_for_end_user(task_model: TaskModel) -> str:
"""Assure any instructions for end user are processed for jinja syntax."""
if task.properties and "instructionsForEndUser" in task.properties:
if task.properties["instructionsForEndUser"]:
extensions = task_model.properties_json['extensions'] if 'extensions' in task_model.properties_json else {}
if extensions and "instructionsForEndUser" in extensions:
if extensions["instructionsForEndUser"]:
try:
instructions = _render_jinja_template(task.properties["instructionsForEndUser"], spiff_task)
task.properties["instructionsForEndUser"] = instructions
instructions = _render_jinja_template(extensions["instructionsForEndUser"], task_model)
extensions["instructionsForEndUser"] = instructions
return instructions
except WorkflowTaskException as wfe:
wfe.add_note("Failed to render instructions for end user.")
@ -397,10 +428,12 @@ def _interstitial_stream(process_instance_id: int) -> Generator[str, Optional[st
processor = ProcessInstanceProcessor(process_instance)
reported_ids = [] # bit of an issue with end tasks showing as getting completed twice.
spiff_task = processor.next_task()
task_model = TaskModel.query.filter_by(guid=str(spiff_task.id)).first()
last_task = None
while last_task != spiff_task:
# import pdb; pdb.set_trace()
task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
instructions = _render_instructions_for_end_user(spiff_task, task)
instructions = _render_instructions_for_end_user(task_model)
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"
@ -408,6 +441,9 @@ def _interstitial_stream(process_instance_id: int) -> Generator[str, Optional[st
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()
task_model = TaskModel.query.filter_by(guid=str(spiff_task.id)).first()
# print(f"spiff_task: {spiff_task}")
# print(f"last_task: {last_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 loop?
@ -446,7 +482,7 @@ def _task_submit_shared(
processor = ProcessInstanceProcessor(process_instance)
spiff_task = _get_spiff_task_from_process_instance(task_guid, process_instance, processor=processor)
AuthorizationService.assert_user_can_complete_spiff_task(process_instance.id, spiff_task, principal.user)
AuthorizationService.assert_user_can_complete_task(process_instance.id, spiff_task.task_spec.name, principal.user)
if spiff_task.state != TaskState.READY:
raise (
@ -634,14 +670,14 @@ def _get_tasks(
return make_response(jsonify(response_json), 200)
def _prepare_form_data(form_file: str, spiff_task: SpiffTask, process_model: ProcessModelInfo) -> dict:
def _prepare_form_data(form_file: str, task_model: TaskModel, process_model: ProcessModelInfo) -> dict:
"""Prepare_form_data."""
if spiff_task.data is None:
if task_model.data is None:
return {}
file_contents = SpecFileService.get_data(process_model, form_file).decode("utf-8")
try:
form_contents = _render_jinja_template(file_contents, spiff_task)
form_contents = _render_jinja_template(file_contents, task_model)
try:
# form_contents is a str
hot_dict: dict = json.loads(form_contents)
@ -661,14 +697,14 @@ def _prepare_form_data(form_file: str, spiff_task: SpiffTask, process_model: Pro
raise api_error
def _render_jinja_template(unprocessed_template: str, spiff_task: SpiffTask) -> str:
def _render_jinja_template(unprocessed_template: str, task_model: TaskModel) -> str:
"""Render_jinja_template."""
jinja_environment = jinja2.Environment(autoescape=True, lstrip_blocks=True, trim_blocks=True)
try:
template = jinja_environment.from_string(unprocessed_template)
return template.render(**spiff_task.data)
return template.render(**(task_model.data or {}))
except jinja2.exceptions.TemplateError as template_error:
wfe = WorkflowTaskException(str(template_error), task=spiff_task, exception=template_error)
wfe = WorkflowTaskException(str(template_error), task=task_model, exception=template_error)
if isinstance(template_error, TemplateSyntaxError):
wfe.line_number = template_error.lineno
wfe.error_line = template_error.source.split("\n")[template_error.lineno - 1]
@ -676,7 +712,7 @@ def _render_jinja_template(unprocessed_template: str, spiff_task: SpiffTask) ->
raise wfe from template_error
except Exception as error:
_type, _value, tb = exc_info()
wfe = WorkflowTaskException(str(error), task=spiff_task, exception=error)
wfe = WorkflowTaskException(str(error), task=task_model, exception=error)
while tb:
if tb.tb_frame.f_code.co_filename == "<template>":
wfe.line_number = tb.tb_lineno
@ -709,9 +745,9 @@ def _get_spiff_task_from_process_instance(
# originally from: https://bitcoden.com/answers/python-nested-dictionary-update-value-where-any-nested-key-matches
def _update_form_schema_with_task_data_as_needed(in_dict: dict, task: Task, spiff_task: SpiffTask) -> None:
def _update_form_schema_with_task_data_as_needed(in_dict: dict, task_model: TaskModel) -> None:
"""Update_nested."""
if task.data is None:
if task_model.data is None:
return None
for k, value in in_dict.items():
@ -724,25 +760,18 @@ def _update_form_schema_with_task_data_as_needed(in_dict: dict, task: Task, spif
if first_element_in_value_list.startswith("options_from_task_data_var:"):
task_data_var = first_element_in_value_list.replace("options_from_task_data_var:", "")
if task_data_var not in task.data:
wte = WorkflowTaskException(
(
"Error building form. Attempting to create a"
" selection list with options from variable"
f" '{task_data_var}' but it doesn't exist in"
" the Task Data."
),
task=spiff_task,
if task_data_var not in task_model.data:
message = (
"Error building form. Attempting to create a selection list with options from variable"
f" '{task_data_var}' but it doesn't exist in the Task Data."
)
raise (
ApiError.from_workflow_exception(
error_code="missing_task_data_var",
message=str(wte),
exp=wte,
)
raise ApiError(
error_code="missing_task_data_var",
message=message,
status_code=500,
)
select_options_from_task_data = task.data.get(task_data_var)
select_options_from_task_data = task_model.data.get(task_data_var)
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):
@ -762,11 +791,11 @@ def _update_form_schema_with_task_data_as_needed(in_dict: dict, task: Task, spif
in_dict[k] = options_for_react_json_schema_form
elif isinstance(value, dict):
_update_form_schema_with_task_data_as_needed(value, task, spiff_task)
_update_form_schema_with_task_data_as_needed(value, task_model)
elif isinstance(value, list):
for o in value:
if isinstance(o, dict):
_update_form_schema_with_task_data_as_needed(o, task, spiff_task)
_update_form_schema_with_task_data_as_needed(o, task_model)
def _get_potential_owner_usernames(assigned_user: AliasedClass) -> Any:
@ -811,15 +840,15 @@ def _find_human_task_or_raise(
return human_task
def _munge_form_ui_schema_based_on_hidden_fields_in_task_data(task: Task) -> None:
if task.form_ui_schema is None:
task.form_ui_schema = {}
def _munge_form_ui_schema_based_on_hidden_fields_in_task_data(task_model: TaskModel) -> None:
if task_model.form_ui_schema is None:
task_model.form_ui_schema = {}
if task.data and "form_ui_hidden_fields" in task.data:
hidden_fields = task.data["form_ui_hidden_fields"]
if task_model.data and "form_ui_hidden_fields" in task_model.data:
hidden_fields = task_model.data["form_ui_hidden_fields"]
for hidden_field in hidden_fields:
hidden_field_parts = hidden_field.split(".")
relevant_depth_of_ui_schema = task.form_ui_schema
relevant_depth_of_ui_schema = task_model.form_ui_schema
for ii, hidden_field_part in enumerate(hidden_field_parts):
if hidden_field_part not in relevant_depth_of_ui_schema:
relevant_depth_of_ui_schema[hidden_field_part] = {}

View File

@ -412,27 +412,27 @@ class AuthorizationService:
) from exception
@staticmethod
def assert_user_can_complete_spiff_task(
def assert_user_can_complete_task(
process_instance_id: int,
spiff_task: SpiffTask,
task_bpmn_identifier: str,
user: UserModel,
) -> bool:
"""Assert_user_can_complete_spiff_task."""
human_task = HumanTaskModel.query.filter_by(
task_name=spiff_task.task_spec.name,
task_name=task_bpmn_identifier,
process_instance_id=process_instance_id,
completed=False,
).first()
if human_task is None:
raise HumanTaskNotFoundError(
f"Could find an human task with task name '{spiff_task.task_spec.name}'"
f"Could find an human task with task name '{task_bpmn_identifier}'"
f" for process instance '{process_instance_id}'"
)
if user not in human_task.potential_owners:
raise UserDoesNotHaveAccessToTaskError(
f"User {user.username} does not have access to update"
f" task'{spiff_task.task_spec.name}' for process instance"
f" task'{task_bpmn_identifier}' for process instance"
f" '{process_instance_id}'"
)
return True

View File

@ -1793,10 +1793,10 @@ class ProcessInstanceProcessor:
# If there are no ready tasks, but the thing isn't complete yet, find the first non-complete task
# and return that
next_task = None
next_task_to_return = None
for task in SpiffTask.Iterator(self.bpmn_process_instance.task_tree, TaskState.NOT_FINISHED_MASK):
next_task = task
return next_task
next_task_to_return = task
return next_task_to_return
def completed_user_tasks(self) -> List[SpiffTask]:
"""Completed_user_tasks."""

View File

@ -344,7 +344,7 @@ class ProcessInstanceService:
data: dict[str, Any],
user: UserModel,
) -> None:
AuthorizationService.assert_user_can_complete_spiff_task(process_instance.id, spiff_task, user)
AuthorizationService.assert_user_can_complete_task(process_instance.id, spiff_task.task_spec.name, user)
cls.save_file_data_and_replace_with_digest_references(
data,
process_instance.id,
@ -442,8 +442,8 @@ class ProcessInstanceService:
# can complete it.
can_complete = False
try:
AuthorizationService.assert_user_can_complete_spiff_task(
processor.process_instance_model.id, spiff_task, g.user
AuthorizationService.assert_user_can_complete_task(
processor.process_instance_model.id, spiff_task.task_spec.name, g.user
)
can_complete = True
except HumanTaskNotFoundError:

View File

@ -488,6 +488,17 @@ class TaskService:
setattr(task_model, task_model_data_column, task_data_hash)
return json_data_dict
@classmethod
def bpmn_process_for_called_activity_or_top_level_process(cls, task_model: TaskModel) -> BpmnProcessModel:
"""Returns either the bpmn process for the call activity calling the process or the top level bpmn process.
For example, process_modelA has processA which has a call activity that calls processB which is inside of process_modelB.
processB has subprocessA which has taskA. Using taskA this method should return processB and then that can be used with
the spec reference cache to find process_modelB.
"""
(bpmn_processes, _task_models) = TaskService.task_models_of_parent_bpmn_processes(task_model, stop_on_first_call_activity=True)
return bpmn_processes[0]
@classmethod
def bpmn_process_and_descendants(cls, bpmn_processes: list[BpmnProcessModel]) -> list[BpmnProcessModel]:
bpmn_process_ids = [p.id for p in bpmn_processes]
@ -500,27 +511,51 @@ class TaskService:
@classmethod
def task_models_of_parent_bpmn_processes(
cls, task_model: TaskModel
cls, task_model: TaskModel, stop_on_first_call_activity: Optional[bool] = False
) -> Tuple[list[BpmnProcessModel], list[TaskModel]]:
"""Returns the list of task models that are associated with the paren bpmn process.
Example: TopLevelProcess has SubprocessTaskA which has CallActivityTaskA which has ScriptTaskA.
SubprocessTaskA corresponds to SpiffSubprocess1.
CallActivityTaskA corresponds to SpiffSubprocess2.
Using ScriptTaskA this will return:
(
[TopLevelProcess, SpiffSubprocess1, SpiffSubprocess2],
[SubprocessTaskA, CallActivityTaskA]
)
If stop_on_first_call_activity it will stop when it reaches the first task model with a type of 'CallActivity'.
This will change the return value in the example to:
(
[SpiffSubprocess2],
[CallActivityTaskA]
)
"""
bpmn_process = task_model.bpmn_process
task_models: list[TaskModel] = []
bpmn_processes: list[BpmnProcessModel] = [bpmn_process]
if bpmn_process.guid is not None:
parent_task_model = TaskModel.query.filter_by(guid=bpmn_process.guid).first()
if parent_task_model is not None:
b, t = cls.task_models_of_parent_bpmn_processes(parent_task_model)
return (bpmn_processes + b, [parent_task_model] + t)
task_models.append(parent_task_model)
if not stop_on_first_call_activity or parent_task_model.task_definition.typename != 'CallActivity':
if parent_task_model is not None:
b, t = cls.task_models_of_parent_bpmn_processes(parent_task_model, stop_on_first_call_activity=stop_on_first_call_activity)
return (b + bpmn_processes, t + task_models)
return (bpmn_processes, task_models)
@classmethod
def full_bpmn_process_path(cls, bpmn_process: BpmnProcessModel) -> list[str]:
"""Returns a list of bpmn process identifiers pointing the given bpmn_process."""
bpmn_process_identifiers: list[str] = [bpmn_process.bpmn_process_definition.bpmn_identifier]
if bpmn_process.direct_parent_process_id is not None:
parent_bpmn_process = BpmnProcessModel.query.filter_by(id=bpmn_process.direct_parent_process_id).first()
if parent_bpmn_process is not None:
# always prepend new identifiers since they come first in the path
bpmn_process_identifiers = cls.full_bpmn_process_path(parent_bpmn_process) + bpmn_process_identifiers
bpmn_process_identifiers: list[str] = []
if bpmn_process.guid:
task_model = TaskModel.query.filter_by(guid=bpmn_process.guid).first()
(
parent_bpmn_processes,
_task_models_of_parent_bpmn_processes,
) = TaskService.task_models_of_parent_bpmn_processes(task_model)
for parent_bpmn_process in parent_bpmn_processes:
bpmn_process_identifiers.append(parent_bpmn_process.bpmn_process_definition.bpmn_identifier)
bpmn_process_identifiers.append(bpmn_process.bpmn_process_definition.bpmn_identifier)
return bpmn_process_identifiers
@classmethod

View File

@ -4,40 +4,88 @@
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1g3dpd7</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1g3dpd7" sourceRef="StartEvent_1" targetRef="do_nothing" />
<bpmn:sequenceFlow id="Flow_1g3dpd7" sourceRef="StartEvent_1" targetRef="level_2b_script_task" />
<bpmn:endEvent id="Event_18dla68">
<bpmn:documentation># Main Workflow
Hello {{my_other_var}}
</bpmn:documentation>
<bpmn:incoming>Flow_0l0w6u9</bpmn:incoming>
<bpmn:incoming>Flow_0wt4dbv</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0l0w6u9" sourceRef="do_nothing" targetRef="Event_18dla68" />
<bpmn:scriptTask id="do_nothing" name="Do Nothing">
<bpmn:scriptTask id="level_2b_script_task" name="level_2b_script_task">
<bpmn:incoming>Flow_1g3dpd7</bpmn:incoming>
<bpmn:outgoing>Flow_0l0w6u9</bpmn:outgoing>
<bpmn:outgoing>Flow_1mvoqe4</bpmn:outgoing>
<bpmn:script>a = 1</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1mvoqe4" sourceRef="level_2b_script_task" targetRef="level_2b_subprocess" />
<bpmn:subProcess id="level_2b_subprocess" name="level_2b_subprocess">
<bpmn:incoming>Flow_1mvoqe4</bpmn:incoming>
<bpmn:outgoing>Flow_0wt4dbv</bpmn:outgoing>
<bpmn:startEvent id="Event_0fpb33c">
<bpmn:outgoing>Flow_18nmqzh</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_18nmqzh" sourceRef="Event_0fpb33c" targetRef="level_2b_subprocess_script_task" />
<bpmn:endEvent id="Event_1x11xe3">
<bpmn:incoming>Flow_1srjuev</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1srjuev" sourceRef="level_2b_subprocess_script_task" targetRef="Event_1x11xe3" />
<bpmn:scriptTask id="level_2b_subprocess_script_task" name="level_2b_subprocess_script_task">
<bpmn:incoming>Flow_18nmqzh</bpmn:incoming>
<bpmn:outgoing>Flow_1srjuev</bpmn:outgoing>
<bpmn:script>z = 1</bpmn:script>
</bpmn:scriptTask>
</bpmn:subProcess>
<bpmn:sequenceFlow id="Flow_0wt4dbv" sourceRef="level_2b_subprocess" targetRef="Event_18dla68" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Level2b">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_18dla68_di" bpmnElement="Event_18dla68">
<dc:Bounds x="432" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1reqred_di" bpmnElement="do_nothing">
<bpmndi:BPMNShape id="Activity_1reqred_di" bpmnElement="level_2b_script_task">
<dc:Bounds x="260" y="77" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_18dla68_di" bpmnElement="Event_18dla68">
<dc:Bounds x="592" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1u9mmh7_di" bpmnElement="level_2b_subprocess">
<dc:Bounds x="410" y="77" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1g3dpd7_di" bpmnElement="Flow_1g3dpd7">
<di:waypoint x="215" y="117" />
<di:waypoint x="260" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0l0w6u9_di" bpmnElement="Flow_0l0w6u9">
<bpmndi:BPMNEdge id="Flow_1mvoqe4_di" bpmnElement="Flow_1mvoqe4">
<di:waypoint x="360" y="117" />
<di:waypoint x="432" y="117" />
<di:waypoint x="410" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0wt4dbv_di" bpmnElement="Flow_0wt4dbv">
<di:waypoint x="510" y="117" />
<di:waypoint x="592" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
<bpmndi:BPMNDiagram id="BPMNDiagram_14p97s9">
<bpmndi:BPMNPlane id="BPMNPlane_1qs3lh3" bpmnElement="level_2b_subprocess">
<bpmndi:BPMNShape id="Event_0fpb33c_di" bpmnElement="Event_0fpb33c">
<dc:Bounds x="332" y="212" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1x11xe3_di" bpmnElement="Event_1x11xe3">
<dc:Bounds x="572" y="212" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0oiioqq_di" bpmnElement="level_2b_subprocess_script_task">
<dc:Bounds x="420" y="190" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_18nmqzh_di" bpmnElement="Flow_18nmqzh">
<di:waypoint x="368" y="230" />
<di:waypoint x="420" y="230" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1srjuev_di" bpmnElement="Flow_1srjuev">
<di:waypoint x="520" y="230" />
<di:waypoint x="572" y="230" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>

View File

@ -4,8 +4,8 @@
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1g3dpd7</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1g3dpd7" sourceRef="StartEvent_1" targetRef="do_nothing" />
<bpmn:sequenceFlow id="Flow_0qdgvah" sourceRef="do_nothing" targetRef="Event_18dla68" />
<bpmn:sequenceFlow id="Flow_1g3dpd7" sourceRef="StartEvent_1" targetRef="level_3_script_task" />
<bpmn:sequenceFlow id="Flow_0qdgvah" sourceRef="level_3_script_task" targetRef="Event_18dla68" />
<bpmn:endEvent id="Event_18dla68">
<bpmn:documentation># Main Workflow
Hello {{my_other_var}}
@ -13,7 +13,7 @@ Hello {{my_other_var}}
</bpmn:documentation>
<bpmn:incoming>Flow_0qdgvah</bpmn:incoming>
</bpmn:endEvent>
<bpmn:scriptTask id="do_nothing" name="Do Nothing">
<bpmn:scriptTask id="level_3_script_task" name="Do Nothing">
<bpmn:incoming>Flow_1g3dpd7</bpmn:incoming>
<bpmn:outgoing>Flow_0qdgvah</bpmn:outgoing>
<bpmn:script>a = 3</bpmn:script>
@ -27,7 +27,7 @@ Hello {{my_other_var}}
<bpmndi:BPMNShape id="Event_18dla68_di" bpmnElement="Event_18dla68">
<dc:Bounds x="432" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1po21cu_di" bpmnElement="do_nothing">
<bpmndi:BPMNShape id="Activity_1po21cu_di" bpmnElement="level_3_script_task">
<dc:Bounds x="280" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1g3dpd7_di" bpmnElement="Flow_1g3dpd7">

View File

@ -0,0 +1,11 @@
{
"description": "",
"display_name": "call activity with nested calls",
"display_order": 0,
"exception_notification_addresses": [],
"fault_or_suspend_on_exception": "fault",
"files": [],
"metadata_extraction_paths": null,
"primary_file_name": "call_activity_nested.bpmn",
"primary_process_id": "Level1"
}

View File

@ -29,12 +29,9 @@ class TestProcessModel(BaseTest):
def test_can_run_process_model_with_call_activities_when_in_same_process_model_directory(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_can_run_process_model_with_call_activities."""
self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group")
process_model = load_test_spec(
"test_group/call_activity_test",
# bpmn_file_name="call_activity_test.bpmn",
@ -49,12 +46,9 @@ class TestProcessModel(BaseTest):
def test_can_run_process_model_with_call_activities_when_not_in_same_directory(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_can_run_process_model_with_call_activities."""
self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group")
process_model = load_test_spec(
"test_group/call_activity_nested",
process_model_source_directory="call_activity_nested",
@ -80,12 +74,9 @@ class TestProcessModel(BaseTest):
def test_can_run_process_model_with_call_activities_when_process_identifier_is_not_in_database(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_can_run_process_model_with_call_activities."""
self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group")
process_model = load_test_spec(
"test_group/call_activity_nested",
process_model_source_directory="call_activity_nested",
@ -116,9 +107,7 @@ class TestProcessModel(BaseTest):
def test_extract_metadata(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_can_run_process_model_with_call_activities."""
process_model = self.create_process_model_with_metadata()

View File

@ -14,12 +14,8 @@ class TestProcessModelService(BaseTest):
def test_can_update_specified_attributes(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_can_update_specified_attributes."""
self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group")
process_model = load_test_spec(
"test_group/hello_world",
bpmn_file_name="hello_world.bpmn",

View File

@ -0,0 +1,153 @@
from flask import Flask
from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel
from flask.testing import FlaskClient
from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
from spiffworkflow_backend.services.task_service import TaskService
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend.models.user import UserModel
class TestTaskService(BaseTest):
def test_can_get_full_bpmn_process_path(
self,
app: Flask,
with_db_and_bpmn_file_cleanup: None,
) -> None:
process_model = load_test_spec(
"test_group/call_activity_nested",
process_model_source_directory="call_activity_nested",
bpmn_file_name="call_activity_nested",
)
bpmn_file_names = [
"call_activity_level_2b",
"call_activity_level_2",
"call_activity_level_3",
]
for bpmn_file_name in bpmn_file_names:
load_test_spec(
f"test_group/{bpmn_file_name}",
process_model_source_directory="call_activity_nested",
bpmn_file_name=bpmn_file_name,
)
process_instance = self.create_process_instance_from_process_model(process_model)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True, execution_strategy_name="greedy")
assert process_instance.status == "complete"
bpmn_process_level_2b = (
BpmnProcessModel.query
.join(BpmnProcessDefinitionModel)
.filter(BpmnProcessDefinitionModel.bpmn_identifier == 'Level2b').first()
)
assert bpmn_process_level_2b is not None
full_bpnmn_process_path = TaskService.full_bpmn_process_path(bpmn_process_level_2b)
assert full_bpnmn_process_path == ['Level1', 'Level2', 'Level2b']
bpmn_process_level_3 = (
BpmnProcessModel.query
.join(BpmnProcessDefinitionModel)
.filter(BpmnProcessDefinitionModel.bpmn_identifier == 'Level3').first()
)
assert bpmn_process_level_3 is not None
full_bpnmn_process_path = TaskService.full_bpmn_process_path(bpmn_process_level_3)
assert full_bpnmn_process_path == ['Level1', 'Level2', 'Level3']
def test_task_models_of_parent_bpmn_processes_stop_on_first_call_activity(
self,
app: Flask,
with_db_and_bpmn_file_cleanup: None,
) -> None:
process_model = load_test_spec(
"test_group/call_activity_nested",
process_model_source_directory="call_activity_nested",
bpmn_file_name="call_activity_nested",
)
bpmn_file_names = [
"call_activity_level_2b",
"call_activity_level_2",
"call_activity_level_3",
]
for bpmn_file_name in bpmn_file_names:
load_test_spec(
f"test_group/{bpmn_file_name}",
process_model_source_directory="call_activity_nested",
bpmn_file_name=bpmn_file_name,
)
process_instance = self.create_process_instance_from_process_model(process_model)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True, execution_strategy_name="greedy")
assert process_instance.status == "complete"
task_model_level_2b = (
TaskModel.query.join(TaskDefinitionModel)
.filter(TaskDefinitionModel.bpmn_identifier == 'level_2b_subprocess_script_task').first()
)
assert task_model_level_2b is not None
(bpmn_processes, task_models) = TaskService.task_models_of_parent_bpmn_processes(task_model_level_2b, stop_on_first_call_activity=True)
assert len(bpmn_processes) == 2
assert len(task_models) == 2
assert bpmn_processes[0].bpmn_process_definition.bpmn_identifier == 'Level2b'
assert task_models[0].task_definition.bpmn_identifier == 'level2b_second_call'
task_model_level_3 = (
TaskModel.query.join(TaskDefinitionModel)
.filter(TaskDefinitionModel.bpmn_identifier == 'level_3_script_task').first()
)
assert task_model_level_3 is not None
(bpmn_processes, task_models) = TaskService.task_models_of_parent_bpmn_processes(task_model_level_3, stop_on_first_call_activity=True)
assert len(bpmn_processes) == 1
assert len(task_models) == 1
assert bpmn_processes[0].bpmn_process_definition.bpmn_identifier == 'Level3'
assert task_models[0].task_definition.bpmn_identifier == 'level3'
def test_bpmn_process_for_called_activity_or_top_level_process(
self,
app: Flask,
with_db_and_bpmn_file_cleanup: None,
) -> None:
process_model = load_test_spec(
"test_group/call_activity_nested",
process_model_source_directory="call_activity_nested",
bpmn_file_name="call_activity_nested",
)
bpmn_file_names = [
"call_activity_level_2b",
"call_activity_level_2",
"call_activity_level_3",
]
for bpmn_file_name in bpmn_file_names:
load_test_spec(
f"test_group/{bpmn_file_name}",
process_model_source_directory="call_activity_nested",
bpmn_file_name=bpmn_file_name,
)
process_instance = self.create_process_instance_from_process_model(process_model)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True, execution_strategy_name="greedy")
assert process_instance.status == "complete"
task_model_level_2b = (
TaskModel.query.join(TaskDefinitionModel)
.filter(TaskDefinitionModel.bpmn_identifier == 'level_2b_subprocess_script_task').first()
)
assert task_model_level_2b is not None
bpmn_process = TaskService.bpmn_process_for_called_activity_or_top_level_process(task_model_level_2b)
assert bpmn_process is not None
assert bpmn_process.bpmn_process_definition.bpmn_identifier == 'Level2b'
task_model_level_3 = (
TaskModel.query.join(TaskDefinitionModel)
.filter(TaskDefinitionModel.bpmn_identifier == 'level_3_script_task').first()
)
assert task_model_level_3 is not None
bpmn_process = TaskService.bpmn_process_for_called_activity_or_top_level_process(task_model_level_3)
assert bpmn_process.bpmn_process_definition.bpmn_identifier == 'Level3'

View File

@ -8,7 +8,10 @@ export default function InstructionsForEndUser({ task }: any) {
}
let instructions =
'There is no additional instructions or information for this task.';
const { properties } = task;
let { properties } = task;
if (!properties) {
properties = task.properties_json;
}
const { instructionsForEndUser } = properties;
if (instructionsForEndUser) {
instructions = instructionsForEndUser;

View File

@ -7,7 +7,6 @@ import MyTasks from './MyTasks';
import CompletedInstances from './CompletedInstances';
import CreateNewInstance from './CreateNewInstance';
import InProgressInstances from './InProgressInstances';
import ProcessInterstitial from './ProcessInterstitial';
export default function HomePageRoutes() {
const location = useLocation();
@ -56,10 +55,6 @@ export default function HomePageRoutes() {
<Route path="my-tasks" element={<MyTasks />} />
<Route path=":process_instance_id/:task_id" element={<TaskShow />} />
<Route path="grouped" element={<InProgressInstances />} />
<Route
path="process/:process_instance_id/interstitial"
element={<ProcessInterstitial />}
/>
<Route path="completed-instances" element={<CompletedInstances />} />
<Route path="create-new-instance" element={<CreateNewInstance />} />
</Routes>

View File

@ -21,6 +21,8 @@ export default function ProcessInterstitial() {
return ['User Task', 'Manual Task'];
}, []);
const processInstanceShowPageBaseUrl = `/admin/process-instances/for-me/${params.process_model_id}`;
useEffect(() => {
fetchEventSource(
`${BACKEND_BASE_URL}/tasks/${params.process_instance_id}`,
@ -127,7 +129,10 @@ export default function ProcessInterstitial() {
entityType: 'process-model-id',
linkLastItem: true,
},
[`Process Instance Id: ${lastTask.process_instance_id}`],
[
`Process Instance: ${params.process_instance_id}`,
`${processInstanceShowPageBaseUrl}/${params.process_instance_id}`,
],
]}
/>
<div style={{ display: 'flex', alignItems: 'center' }}>

View File

@ -6,7 +6,7 @@ export default function ProcessRoutes() {
return (
<Routes>
<Route
path=":process_model_identifier/:process_instance_id/interstitial"
path=":process_model_id/:process_instance_id/interstitial"
element={<ProcessInterstitial />}
/>
</Routes>