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:
parent
f4af53f1dd
commit
ddf8283c71
|
@ -69,6 +69,17 @@ class TaskModel(SpiffworkflowBaseDBModel):
|
||||||
|
|
||||||
data: Optional[dict] = None
|
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:
|
def python_env_data(self) -> dict:
|
||||||
return JsonDataModel.find_data_dict_by_hash(self.python_env_data_hash)
|
return JsonDataModel.find_data_dict_by_hash(self.python_env_data_hash)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +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
|
||||||
|
from spiffworkflow_backend.services.authorization_service import UserDoesNotHaveAccessToTaskError
|
||||||
|
from spiffworkflow_backend.services.authorization_service import HumanTaskNotFoundError
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from sys import exc_info
|
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_schema_file_name = ""
|
||||||
form_ui_schema_file_name = ""
|
form_ui_schema_file_name = ""
|
||||||
|
|
||||||
processor = ProcessInstanceProcessor(process_instance)
|
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
|
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:
|
if "properties" in extensions:
|
||||||
properties = extensions["properties"]
|
properties = extensions["properties"]
|
||||||
if "formJsonSchemaFilename" in properties:
|
if "formJsonSchemaFilename" in properties:
|
||||||
form_schema_file_name = properties["formJsonSchemaFilename"]
|
form_schema_file_name = properties["formJsonSchemaFilename"]
|
||||||
if "formUiSchemaFilename" in properties:
|
if "formUiSchemaFilename" in properties:
|
||||||
form_ui_schema_file_name = properties["formUiSchemaFilename"]
|
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 = ProcessInstanceService.spiff_task_to_api_task(processor, spiff_task)
|
||||||
task.data = spiff_task.data
|
task.data = spiff_task.data
|
||||||
task.process_model_display_name = process_model.display_name
|
task.process_model_display_name = process_model.display_name
|
||||||
task.process_model_identifier = process_model.id
|
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
|
process_model_with_form = process_model
|
||||||
|
|
||||||
refs = SpecFileService.get_references_for_process(process_model_with_form)
|
refs = SpecFileService.get_references_for_process(process_model_with_form)
|
||||||
all_processes = [i.identifier for i in refs]
|
all_processes = [i.identifier for i in refs]
|
||||||
if task.process_identifier not in all_processes:
|
if task_process_identifier not in all_processes:
|
||||||
top_process_name = processor.find_process_model_process_name_by_task_name(task.process_identifier)
|
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(
|
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())
|
relative_path = os.path.relpath(bpmn_file_full_path, start=FileSystemService.root_path())
|
||||||
process_model_relative_path = os.path.dirname(relative_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)
|
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:
|
if not form_schema_file_name:
|
||||||
raise (
|
raise (
|
||||||
ApiError(
|
ApiError(
|
||||||
|
@ -330,37 +360,38 @@ def task_show(process_instance_id: int, task_guid: str = "next") -> flask.wrappe
|
||||||
|
|
||||||
form_dict = _prepare_form_data(
|
form_dict = _prepare_form_data(
|
||||||
form_schema_file_name,
|
form_schema_file_name,
|
||||||
spiff_task,
|
task_model,
|
||||||
process_model_with_form,
|
process_model_with_form,
|
||||||
)
|
)
|
||||||
|
|
||||||
if task.data:
|
if task_model.data:
|
||||||
_update_form_schema_with_task_data_as_needed(form_dict, task, spiff_task)
|
_update_form_schema_with_task_data_as_needed(form_dict, task_model)
|
||||||
|
|
||||||
if form_dict:
|
if form_dict:
|
||||||
task.form_schema = form_dict
|
task_model.form_schema = form_dict
|
||||||
|
|
||||||
if form_ui_schema_file_name:
|
if form_ui_schema_file_name:
|
||||||
ui_form_contents = _prepare_form_data(
|
ui_form_contents = _prepare_form_data(
|
||||||
form_ui_schema_file_name,
|
form_ui_schema_file_name,
|
||||||
task,
|
task_model,
|
||||||
process_model_with_form,
|
process_model_with_form,
|
||||||
)
|
)
|
||||||
if ui_form_contents:
|
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)
|
_munge_form_ui_schema_based_on_hidden_fields_in_task_data(task_model)
|
||||||
_render_instructions_for_end_user(spiff_task, task)
|
_render_instructions_for_end_user(task_model)
|
||||||
return make_response(jsonify(task), 200)
|
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."""
|
"""Assure any instructions for end user are processed for jinja syntax."""
|
||||||
if task.properties and "instructionsForEndUser" in task.properties:
|
extensions = task_model.properties_json['extensions'] if 'extensions' in task_model.properties_json else {}
|
||||||
if task.properties["instructionsForEndUser"]:
|
if extensions and "instructionsForEndUser" in extensions:
|
||||||
|
if extensions["instructionsForEndUser"]:
|
||||||
try:
|
try:
|
||||||
instructions = _render_jinja_template(task.properties["instructionsForEndUser"], spiff_task)
|
instructions = _render_jinja_template(extensions["instructionsForEndUser"], task_model)
|
||||||
task.properties["instructionsForEndUser"] = instructions
|
extensions["instructionsForEndUser"] = instructions
|
||||||
return instructions
|
return instructions
|
||||||
except WorkflowTaskException as wfe:
|
except WorkflowTaskException as wfe:
|
||||||
wfe.add_note("Failed to render instructions for end user.")
|
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)
|
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.
|
||||||
spiff_task = processor.next_task()
|
spiff_task = processor.next_task()
|
||||||
|
task_model = TaskModel.query.filter_by(guid=str(spiff_task.id)).first()
|
||||||
last_task = None
|
last_task = None
|
||||||
while last_task != spiff_task:
|
while last_task != spiff_task:
|
||||||
|
# import pdb; pdb.set_trace()
|
||||||
task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
|
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:
|
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"
|
||||||
|
@ -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="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")
|
||||||
spiff_task = processor.next_task()
|
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,
|
# Note, this has to be done in case someone leaves the page,
|
||||||
# which can otherwise cancel this function and leave completed tasks un-registered.
|
# 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?
|
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)
|
processor = ProcessInstanceProcessor(process_instance)
|
||||||
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)
|
||||||
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:
|
if spiff_task.state != TaskState.READY:
|
||||||
raise (
|
raise (
|
||||||
|
@ -634,14 +670,14 @@ def _get_tasks(
|
||||||
return make_response(jsonify(response_json), 200)
|
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."""
|
"""Prepare_form_data."""
|
||||||
if spiff_task.data is None:
|
if task_model.data is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
file_contents = SpecFileService.get_data(process_model, form_file).decode("utf-8")
|
file_contents = SpecFileService.get_data(process_model, form_file).decode("utf-8")
|
||||||
try:
|
try:
|
||||||
form_contents = _render_jinja_template(file_contents, spiff_task)
|
form_contents = _render_jinja_template(file_contents, task_model)
|
||||||
try:
|
try:
|
||||||
# form_contents is a str
|
# form_contents is a str
|
||||||
hot_dict: dict = json.loads(form_contents)
|
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
|
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."""
|
"""Render_jinja_template."""
|
||||||
jinja_environment = jinja2.Environment(autoescape=True, lstrip_blocks=True, trim_blocks=True)
|
jinja_environment = jinja2.Environment(autoescape=True, lstrip_blocks=True, trim_blocks=True)
|
||||||
try:
|
try:
|
||||||
template = jinja_environment.from_string(unprocessed_template)
|
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:
|
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):
|
if isinstance(template_error, TemplateSyntaxError):
|
||||||
wfe.line_number = template_error.lineno
|
wfe.line_number = template_error.lineno
|
||||||
wfe.error_line = template_error.source.split("\n")[template_error.lineno - 1]
|
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
|
raise wfe from template_error
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
_type, _value, tb = exc_info()
|
_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:
|
while tb:
|
||||||
if tb.tb_frame.f_code.co_filename == "<template>":
|
if tb.tb_frame.f_code.co_filename == "<template>":
|
||||||
wfe.line_number = tb.tb_lineno
|
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
|
# 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."""
|
"""Update_nested."""
|
||||||
if task.data is None:
|
if task_model.data is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
for k, value in in_dict.items():
|
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:"):
|
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:", "")
|
task_data_var = first_element_in_value_list.replace("options_from_task_data_var:", "")
|
||||||
|
|
||||||
if task_data_var not in task.data:
|
if task_data_var not in task_model.data:
|
||||||
wte = WorkflowTaskException(
|
message = (
|
||||||
(
|
"Error building form. Attempting to create a selection list with options from variable"
|
||||||
"Error building form. Attempting to create a"
|
f" '{task_data_var}' but it doesn't exist in the Task Data."
|
||||||
" selection list with options from variable"
|
|
||||||
f" '{task_data_var}' but it doesn't exist in"
|
|
||||||
" the Task Data."
|
|
||||||
),
|
|
||||||
task=spiff_task,
|
|
||||||
)
|
)
|
||||||
raise (
|
raise ApiError(
|
||||||
ApiError.from_workflow_exception(
|
|
||||||
error_code="missing_task_data_var",
|
error_code="missing_task_data_var",
|
||||||
message=str(wte),
|
message=message,
|
||||||
exp=wte,
|
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 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):
|
||||||
|
|
||||||
|
@ -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
|
in_dict[k] = options_for_react_json_schema_form
|
||||||
elif isinstance(value, dict):
|
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):
|
elif isinstance(value, list):
|
||||||
for o in value:
|
for o in value:
|
||||||
if isinstance(o, dict):
|
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:
|
def _get_potential_owner_usernames(assigned_user: AliasedClass) -> Any:
|
||||||
|
@ -811,15 +840,15 @@ def _find_human_task_or_raise(
|
||||||
return human_task
|
return human_task
|
||||||
|
|
||||||
|
|
||||||
def _munge_form_ui_schema_based_on_hidden_fields_in_task_data(task: Task) -> None:
|
def _munge_form_ui_schema_based_on_hidden_fields_in_task_data(task_model: TaskModel) -> None:
|
||||||
if task.form_ui_schema is None:
|
if task_model.form_ui_schema is None:
|
||||||
task.form_ui_schema = {}
|
task_model.form_ui_schema = {}
|
||||||
|
|
||||||
if task.data and "form_ui_hidden_fields" in task.data:
|
if task_model.data and "form_ui_hidden_fields" in task_model.data:
|
||||||
hidden_fields = task.data["form_ui_hidden_fields"]
|
hidden_fields = task_model.data["form_ui_hidden_fields"]
|
||||||
for hidden_field in hidden_fields:
|
for hidden_field in hidden_fields:
|
||||||
hidden_field_parts = hidden_field.split(".")
|
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):
|
for ii, hidden_field_part in enumerate(hidden_field_parts):
|
||||||
if hidden_field_part not in relevant_depth_of_ui_schema:
|
if hidden_field_part not in relevant_depth_of_ui_schema:
|
||||||
relevant_depth_of_ui_schema[hidden_field_part] = {}
|
relevant_depth_of_ui_schema[hidden_field_part] = {}
|
||||||
|
|
|
@ -412,27 +412,27 @@ class AuthorizationService:
|
||||||
) from exception
|
) from exception
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def assert_user_can_complete_spiff_task(
|
def assert_user_can_complete_task(
|
||||||
process_instance_id: int,
|
process_instance_id: int,
|
||||||
spiff_task: SpiffTask,
|
task_bpmn_identifier: str,
|
||||||
user: UserModel,
|
user: UserModel,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Assert_user_can_complete_spiff_task."""
|
"""Assert_user_can_complete_spiff_task."""
|
||||||
human_task = HumanTaskModel.query.filter_by(
|
human_task = HumanTaskModel.query.filter_by(
|
||||||
task_name=spiff_task.task_spec.name,
|
task_name=task_bpmn_identifier,
|
||||||
process_instance_id=process_instance_id,
|
process_instance_id=process_instance_id,
|
||||||
completed=False,
|
completed=False,
|
||||||
).first()
|
).first()
|
||||||
if human_task is None:
|
if human_task is None:
|
||||||
raise HumanTaskNotFoundError(
|
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}'"
|
f" for process instance '{process_instance_id}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
if user not in human_task.potential_owners:
|
if user not in human_task.potential_owners:
|
||||||
raise UserDoesNotHaveAccessToTaskError(
|
raise UserDoesNotHaveAccessToTaskError(
|
||||||
f"User {user.username} does not have access to update"
|
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}'"
|
f" '{process_instance_id}'"
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -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
|
# If there are no ready tasks, but the thing isn't complete yet, find the first non-complete task
|
||||||
# and return that
|
# 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):
|
for task in SpiffTask.Iterator(self.bpmn_process_instance.task_tree, TaskState.NOT_FINISHED_MASK):
|
||||||
next_task = task
|
next_task_to_return = task
|
||||||
return next_task
|
return next_task_to_return
|
||||||
|
|
||||||
def completed_user_tasks(self) -> List[SpiffTask]:
|
def completed_user_tasks(self) -> List[SpiffTask]:
|
||||||
"""Completed_user_tasks."""
|
"""Completed_user_tasks."""
|
||||||
|
|
|
@ -344,7 +344,7 @@ class ProcessInstanceService:
|
||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
user: UserModel,
|
user: UserModel,
|
||||||
) -> None:
|
) -> 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(
|
cls.save_file_data_and_replace_with_digest_references(
|
||||||
data,
|
data,
|
||||||
process_instance.id,
|
process_instance.id,
|
||||||
|
@ -442,8 +442,8 @@ class ProcessInstanceService:
|
||||||
# can complete it.
|
# can complete it.
|
||||||
can_complete = False
|
can_complete = False
|
||||||
try:
|
try:
|
||||||
AuthorizationService.assert_user_can_complete_spiff_task(
|
AuthorizationService.assert_user_can_complete_task(
|
||||||
processor.process_instance_model.id, spiff_task, g.user
|
processor.process_instance_model.id, spiff_task.task_spec.name, g.user
|
||||||
)
|
)
|
||||||
can_complete = True
|
can_complete = True
|
||||||
except HumanTaskNotFoundError:
|
except HumanTaskNotFoundError:
|
||||||
|
|
|
@ -488,6 +488,17 @@ class TaskService:
|
||||||
setattr(task_model, task_model_data_column, task_data_hash)
|
setattr(task_model, task_model_data_column, task_data_hash)
|
||||||
return json_data_dict
|
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
|
@classmethod
|
||||||
def bpmn_process_and_descendants(cls, bpmn_processes: list[BpmnProcessModel]) -> list[BpmnProcessModel]:
|
def bpmn_process_and_descendants(cls, bpmn_processes: list[BpmnProcessModel]) -> list[BpmnProcessModel]:
|
||||||
bpmn_process_ids = [p.id for p in bpmn_processes]
|
bpmn_process_ids = [p.id for p in bpmn_processes]
|
||||||
|
@ -500,27 +511,51 @@ class TaskService:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def task_models_of_parent_bpmn_processes(
|
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]]:
|
) -> 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
|
bpmn_process = task_model.bpmn_process
|
||||||
task_models: list[TaskModel] = []
|
task_models: list[TaskModel] = []
|
||||||
bpmn_processes: list[BpmnProcessModel] = [bpmn_process]
|
bpmn_processes: list[BpmnProcessModel] = [bpmn_process]
|
||||||
if bpmn_process.guid is not None:
|
if bpmn_process.guid is not None:
|
||||||
parent_task_model = TaskModel.query.filter_by(guid=bpmn_process.guid).first()
|
parent_task_model = TaskModel.query.filter_by(guid=bpmn_process.guid).first()
|
||||||
|
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:
|
if parent_task_model is not None:
|
||||||
b, t = cls.task_models_of_parent_bpmn_processes(parent_task_model)
|
b, t = cls.task_models_of_parent_bpmn_processes(parent_task_model, stop_on_first_call_activity=stop_on_first_call_activity)
|
||||||
return (bpmn_processes + b, [parent_task_model] + t)
|
return (b + bpmn_processes, t + task_models)
|
||||||
return (bpmn_processes, task_models)
|
return (bpmn_processes, task_models)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def full_bpmn_process_path(cls, bpmn_process: BpmnProcessModel) -> list[str]:
|
def full_bpmn_process_path(cls, bpmn_process: BpmnProcessModel) -> list[str]:
|
||||||
"""Returns a list of bpmn process identifiers pointing the given bpmn_process."""
|
"""Returns a list of bpmn process identifiers pointing the given bpmn_process."""
|
||||||
bpmn_process_identifiers: list[str] = [bpmn_process.bpmn_process_definition.bpmn_identifier]
|
bpmn_process_identifiers: list[str] = []
|
||||||
if bpmn_process.direct_parent_process_id is not None:
|
if bpmn_process.guid:
|
||||||
parent_bpmn_process = BpmnProcessModel.query.filter_by(id=bpmn_process.direct_parent_process_id).first()
|
task_model = TaskModel.query.filter_by(guid=bpmn_process.guid).first()
|
||||||
if parent_bpmn_process is not None:
|
(
|
||||||
# always prepend new identifiers since they come first in the path
|
parent_bpmn_processes,
|
||||||
bpmn_process_identifiers = cls.full_bpmn_process_path(parent_bpmn_process) + bpmn_process_identifiers
|
_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
|
return bpmn_process_identifiers
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -4,40 +4,88 @@
|
||||||
<bpmn:startEvent id="StartEvent_1">
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
<bpmn:outgoing>Flow_1g3dpd7</bpmn:outgoing>
|
<bpmn:outgoing>Flow_1g3dpd7</bpmn:outgoing>
|
||||||
</bpmn:startEvent>
|
</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:endEvent id="Event_18dla68">
|
||||||
<bpmn:documentation># Main Workflow
|
<bpmn:documentation># Main Workflow
|
||||||
Hello {{my_other_var}}
|
Hello {{my_other_var}}
|
||||||
|
|
||||||
</bpmn:documentation>
|
</bpmn:documentation>
|
||||||
<bpmn:incoming>Flow_0l0w6u9</bpmn:incoming>
|
<bpmn:incoming>Flow_0wt4dbv</bpmn:incoming>
|
||||||
</bpmn:endEvent>
|
</bpmn:endEvent>
|
||||||
<bpmn:sequenceFlow id="Flow_0l0w6u9" sourceRef="do_nothing" targetRef="Event_18dla68" />
|
<bpmn:scriptTask id="level_2b_script_task" name="level_2b_script_task">
|
||||||
<bpmn:scriptTask id="do_nothing" name="Do Nothing">
|
|
||||||
<bpmn:incoming>Flow_1g3dpd7</bpmn:incoming>
|
<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:script>a = 1</bpmn:script>
|
||||||
</bpmn:scriptTask>
|
</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>
|
</bpmn:process>
|
||||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Level2b">
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Level2b">
|
||||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||||
<dc:Bounds x="179" y="99" width="36" height="36" />
|
<dc:Bounds x="179" y="99" width="36" height="36" />
|
||||||
</bpmndi:BPMNShape>
|
</bpmndi:BPMNShape>
|
||||||
<bpmndi:BPMNShape id="Event_18dla68_di" bpmnElement="Event_18dla68">
|
<bpmndi:BPMNShape id="Activity_1reqred_di" bpmnElement="level_2b_script_task">
|
||||||
<dc:Bounds x="432" y="99" width="36" height="36" />
|
|
||||||
</bpmndi:BPMNShape>
|
|
||||||
<bpmndi:BPMNShape id="Activity_1reqred_di" bpmnElement="do_nothing">
|
|
||||||
<dc:Bounds x="260" y="77" width="100" height="80" />
|
<dc:Bounds x="260" y="77" width="100" height="80" />
|
||||||
<bpmndi:BPMNLabel />
|
<bpmndi:BPMNLabel />
|
||||||
</bpmndi:BPMNShape>
|
</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">
|
<bpmndi:BPMNEdge id="Flow_1g3dpd7_di" bpmnElement="Flow_1g3dpd7">
|
||||||
<di:waypoint x="215" y="117" />
|
<di:waypoint x="215" y="117" />
|
||||||
<di:waypoint x="260" y="117" />
|
<di:waypoint x="260" y="117" />
|
||||||
</bpmndi:BPMNEdge>
|
</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="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:BPMNEdge>
|
||||||
</bpmndi:BPMNPlane>
|
</bpmndi:BPMNPlane>
|
||||||
</bpmndi:BPMNDiagram>
|
</bpmndi:BPMNDiagram>
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
<bpmn:startEvent id="StartEvent_1">
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
<bpmn:outgoing>Flow_1g3dpd7</bpmn:outgoing>
|
<bpmn:outgoing>Flow_1g3dpd7</bpmn:outgoing>
|
||||||
</bpmn:startEvent>
|
</bpmn:startEvent>
|
||||||
<bpmn:sequenceFlow id="Flow_1g3dpd7" sourceRef="StartEvent_1" targetRef="do_nothing" />
|
<bpmn:sequenceFlow id="Flow_1g3dpd7" sourceRef="StartEvent_1" targetRef="level_3_script_task" />
|
||||||
<bpmn:sequenceFlow id="Flow_0qdgvah" sourceRef="do_nothing" targetRef="Event_18dla68" />
|
<bpmn:sequenceFlow id="Flow_0qdgvah" sourceRef="level_3_script_task" targetRef="Event_18dla68" />
|
||||||
<bpmn:endEvent id="Event_18dla68">
|
<bpmn:endEvent id="Event_18dla68">
|
||||||
<bpmn:documentation># Main Workflow
|
<bpmn:documentation># Main Workflow
|
||||||
Hello {{my_other_var}}
|
Hello {{my_other_var}}
|
||||||
|
@ -13,7 +13,7 @@ Hello {{my_other_var}}
|
||||||
</bpmn:documentation>
|
</bpmn:documentation>
|
||||||
<bpmn:incoming>Flow_0qdgvah</bpmn:incoming>
|
<bpmn:incoming>Flow_0qdgvah</bpmn:incoming>
|
||||||
</bpmn:endEvent>
|
</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:incoming>Flow_1g3dpd7</bpmn:incoming>
|
||||||
<bpmn:outgoing>Flow_0qdgvah</bpmn:outgoing>
|
<bpmn:outgoing>Flow_0qdgvah</bpmn:outgoing>
|
||||||
<bpmn:script>a = 3</bpmn:script>
|
<bpmn:script>a = 3</bpmn:script>
|
||||||
|
@ -27,7 +27,7 @@ Hello {{my_other_var}}
|
||||||
<bpmndi:BPMNShape id="Event_18dla68_di" bpmnElement="Event_18dla68">
|
<bpmndi:BPMNShape id="Event_18dla68_di" bpmnElement="Event_18dla68">
|
||||||
<dc:Bounds x="432" y="99" width="36" height="36" />
|
<dc:Bounds x="432" y="99" width="36" height="36" />
|
||||||
</bpmndi:BPMNShape>
|
</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" />
|
<dc:Bounds x="280" y="77" width="100" height="80" />
|
||||||
</bpmndi:BPMNShape>
|
</bpmndi:BPMNShape>
|
||||||
<bpmndi:BPMNEdge id="Flow_1g3dpd7_di" bpmnElement="Flow_1g3dpd7">
|
<bpmndi:BPMNEdge id="Flow_1g3dpd7_di" bpmnElement="Flow_1g3dpd7">
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
{}
|
|
|
@ -29,12 +29,9 @@ class TestProcessModel(BaseTest):
|
||||||
def test_can_run_process_model_with_call_activities_when_in_same_process_model_directory(
|
def test_can_run_process_model_with_call_activities_when_in_same_process_model_directory(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
client: FlaskClient,
|
|
||||||
with_db_and_bpmn_file_cleanup: None,
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
with_super_admin_user: UserModel,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_can_run_process_model_with_call_activities."""
|
"""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(
|
process_model = load_test_spec(
|
||||||
"test_group/call_activity_test",
|
"test_group/call_activity_test",
|
||||||
# bpmn_file_name="call_activity_test.bpmn",
|
# 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(
|
def test_can_run_process_model_with_call_activities_when_not_in_same_directory(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
client: FlaskClient,
|
|
||||||
with_db_and_bpmn_file_cleanup: None,
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
with_super_admin_user: UserModel,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_can_run_process_model_with_call_activities."""
|
"""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(
|
process_model = load_test_spec(
|
||||||
"test_group/call_activity_nested",
|
"test_group/call_activity_nested",
|
||||||
process_model_source_directory="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(
|
def test_can_run_process_model_with_call_activities_when_process_identifier_is_not_in_database(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
client: FlaskClient,
|
|
||||||
with_db_and_bpmn_file_cleanup: None,
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
with_super_admin_user: UserModel,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_can_run_process_model_with_call_activities."""
|
"""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(
|
process_model = load_test_spec(
|
||||||
"test_group/call_activity_nested",
|
"test_group/call_activity_nested",
|
||||||
process_model_source_directory="call_activity_nested",
|
process_model_source_directory="call_activity_nested",
|
||||||
|
@ -116,9 +107,7 @@ class TestProcessModel(BaseTest):
|
||||||
def test_extract_metadata(
|
def test_extract_metadata(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
client: FlaskClient,
|
|
||||||
with_db_and_bpmn_file_cleanup: None,
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
with_super_admin_user: UserModel,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test_can_run_process_model_with_call_activities."""
|
"""Test_can_run_process_model_with_call_activities."""
|
||||||
process_model = self.create_process_model_with_metadata()
|
process_model = self.create_process_model_with_metadata()
|
||||||
|
|
|
@ -14,12 +14,8 @@ class TestProcessModelService(BaseTest):
|
||||||
def test_can_update_specified_attributes(
|
def test_can_update_specified_attributes(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
client: FlaskClient,
|
|
||||||
with_db_and_bpmn_file_cleanup: None,
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
with_super_admin_user: UserModel,
|
|
||||||
) -> None:
|
) -> 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(
|
process_model = load_test_spec(
|
||||||
"test_group/hello_world",
|
"test_group/hello_world",
|
||||||
bpmn_file_name="hello_world.bpmn",
|
bpmn_file_name="hello_world.bpmn",
|
||||||
|
|
|
@ -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'
|
|
@ -8,7 +8,10 @@ export default function InstructionsForEndUser({ task }: any) {
|
||||||
}
|
}
|
||||||
let instructions =
|
let instructions =
|
||||||
'There is no additional instructions or information for this task.';
|
'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;
|
const { instructionsForEndUser } = properties;
|
||||||
if (instructionsForEndUser) {
|
if (instructionsForEndUser) {
|
||||||
instructions = instructionsForEndUser;
|
instructions = instructionsForEndUser;
|
||||||
|
|
|
@ -7,7 +7,6 @@ import MyTasks from './MyTasks';
|
||||||
import CompletedInstances from './CompletedInstances';
|
import CompletedInstances from './CompletedInstances';
|
||||||
import CreateNewInstance from './CreateNewInstance';
|
import CreateNewInstance from './CreateNewInstance';
|
||||||
import InProgressInstances from './InProgressInstances';
|
import InProgressInstances from './InProgressInstances';
|
||||||
import ProcessInterstitial from './ProcessInterstitial';
|
|
||||||
|
|
||||||
export default function HomePageRoutes() {
|
export default function HomePageRoutes() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
@ -56,10 +55,6 @@ 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="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>
|
||||||
|
|
|
@ -21,6 +21,8 @@ export default function ProcessInterstitial() {
|
||||||
return ['User Task', 'Manual Task'];
|
return ['User Task', 'Manual Task'];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const processInstanceShowPageBaseUrl = `/admin/process-instances/for-me/${params.process_model_id}`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchEventSource(
|
fetchEventSource(
|
||||||
`${BACKEND_BASE_URL}/tasks/${params.process_instance_id}`,
|
`${BACKEND_BASE_URL}/tasks/${params.process_instance_id}`,
|
||||||
|
@ -127,7 +129,10 @@ export default function ProcessInterstitial() {
|
||||||
entityType: 'process-model-id',
|
entityType: 'process-model-id',
|
||||||
linkLastItem: true,
|
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' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
|
|
@ -6,7 +6,7 @@ export default function ProcessRoutes() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path=":process_model_identifier/:process_instance_id/interstitial"
|
path=":process_model_id/:process_instance_id/interstitial"
|
||||||
element={<ProcessInterstitial />}
|
element={<ProcessInterstitial />}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
Loading…
Reference in New Issue