From c3cb2888750f9b77b0bd0380a0b3265fa0efc14f Mon Sep 17 00:00:00 2001 From: burnettk Date: Fri, 3 Feb 2023 15:40:14 -0500 Subject: [PATCH 1/3] make form schema and form ui schema both dicts, add support for hiding fields based on task data --- .../realm_exports/spiffworkflow-realm.json | 2 +- .../src/spiffworkflow_backend/models/task.py | 4 +- .../routes/tasks_controller.py | 57 ++++++++++++------- .../color_question.json | 4 ++ .../dynamic_enums_ask_for_color.bpmn | 4 +- .../integration/test_process_api.py | 3 + .../src/routes/TaskShow.tsx | 2 +- 7 files changed, 51 insertions(+), 25 deletions(-) diff --git a/spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-realm.json b/spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-realm.json index 722f1276..eab3bd96 100644 --- a/spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-realm.json +++ b/spiffworkflow-backend/keycloak/realm_exports/spiffworkflow-realm.json @@ -3554,4 +3554,4 @@ "clientPolicies" : { "policies" : [ ] } -} \ No newline at end of file +} diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py index 5c924196..e1851773 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py @@ -115,8 +115,8 @@ class Task: process_model_display_name: Union[str, None] = None, process_group_identifier: Union[str, None] = None, process_model_identifier: Union[str, None] = None, - form_schema: Union[str, None] = None, - form_ui_schema: Union[str, None] = None, + form_schema: Union[dict, None] = None, + form_ui_schema: Union[dict, None] = None, parent: Optional[str] = None, event_definition: Union[dict[str, Any], None] = None, call_activity_process_identifier: Optional[str] = None, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index 2879c120..086f7a45 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -253,31 +253,16 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response ) ) - form_contents = _prepare_form_data( + form_dict = _prepare_form_data( form_schema_file_name, spiff_task, process_model_with_form, ) - try: - # form_contents is a str - form_dict = json.loads(form_contents) - except Exception as exception: - raise ( - ApiError( - error_code="error_loading_form", - message=( - f"Could not load form schema from: {form_schema_file_name}." - f" Error was: {str(exception)}" - ), - status_code=400, - ) - ) from exception - if task.data: _update_form_schema_with_task_data_as_needed(form_dict, task) - if form_contents: + if form_dict: task.form_schema = form_dict if form_ui_schema_file_name: @@ -289,6 +274,23 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response if ui_form_contents: task.form_ui_schema = ui_form_contents + if task.form_ui_schema is None: + task.form_ui_schema = {} + + if task.data and "form_ui_hidden_fields" in task.data: + hidden_fields = task.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 + 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] = {} + relevant_depth_of_ui_schema = relevant_depth_of_ui_schema[ + hidden_field_part + ] + if len(hidden_field_parts) == ii + 1: + relevant_depth_of_ui_schema["ui:widget"] = "hidden" + if task.properties and task.data and "instructionsForEndUser" in task.properties: if task.properties["instructionsForEndUser"]: try: @@ -525,14 +527,29 @@ def _get_tasks( def _prepare_form_data( form_file: str, spiff_task: SpiffTask, process_model: ProcessModelInfo -) -> str: +) -> dict: """Prepare_form_data.""" if spiff_task.data is None: - return "" + return {} file_contents = SpecFileService.get_data(process_model, form_file).decode("utf-8") try: - return _render_jinja_template(file_contents, spiff_task) + form_contents = _render_jinja_template(file_contents, spiff_task) + try: + # form_contents is a str + hot_dict: dict = json.loads(form_contents) + return hot_dict + except Exception as exception: + raise ( + ApiError( + error_code="error_loading_form", + message=( + f"Could not load form schema from: {form_file}." + f" Error was: {str(exception)}" + ), + status_code=400, + ) + ) from exception except WorkflowTaskException as wfe: wfe.add_note(f"Error in Json Form File '{form_file}'") api_error = ApiError.from_workflow_exception( diff --git a/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/color_question.json b/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/color_question.json index 20ea1c12..a3528138 100644 --- a/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/color_question.json +++ b/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/color_question.json @@ -13,6 +13,10 @@ "selectedColor": { "$ref": "#/definitions/Color", "title": "Select color" + }, + "veryImportantFieldButOnlySometimes": { + "title": "Very important field", + "type": "string" } } } diff --git a/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/dynamic_enums_ask_for_color.bpmn b/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/dynamic_enums_ask_for_color.bpmn index 7d21851b..7ec50272 100644 --- a/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/dynamic_enums_ask_for_color.bpmn +++ b/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/dynamic_enums_ask_for_color.bpmn @@ -13,7 +13,9 @@ Flow_1my9ag5 Flow_0b04rbg - awesome_color_options = [{"value": "blue", "label": "Blue"}, {"value": "green", "label": "Green"}] + awesome_color_options = [{"value": "blue", "label": "Blue"}, {"value": "green", "label": "Green"}] +form_ui_hidden_fields = ["veryImportantFieldButOnlySometimes"] + diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index f52cbc43..91246c31 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -1686,6 +1686,9 @@ class TestProcessApi(BaseTest): response.json["form_schema"]["definitions"]["Color"]["anyOf"][1]["title"] == "Green" ) + assert response.json["form_ui_schema"] == { + "veryImportantFieldButOnlySometimes": {"ui:widget": "hidden"} + } def test_process_instance_list_with_default_list( self, diff --git a/spiffworkflow-frontend/src/routes/TaskShow.tsx b/spiffworkflow-frontend/src/routes/TaskShow.tsx index 83e5df3f..fc50df7e 100644 --- a/spiffworkflow-frontend/src/routes/TaskShow.tsx +++ b/spiffworkflow-frontend/src/routes/TaskShow.tsx @@ -189,7 +189,7 @@ export default function TaskShow() { }, }; } else if (task.form_ui_schema) { - formUiSchema = JSON.parse(task.form_ui_schema); + formUiSchema = task.form_ui_schema; } if (task.state !== 'READY') { formUiSchema = Object.assign(formUiSchema || {}, { From 408759d12201b636f30722c638c832c42b4e5c36 Mon Sep 17 00:00:00 2001 From: burnettk Date: Fri, 3 Feb 2023 15:47:35 -0500 Subject: [PATCH 2/3] show that hiding nested fields works as well --- .../data/dynamic_enum_select_fields/color_question.json | 8 ++++++++ .../dynamic_enums_ask_for_color.bpmn | 2 +- .../spiffworkflow_backend/integration/test_process_api.py | 7 ++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/color_question.json b/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/color_question.json index a3528138..1ce7072b 100644 --- a/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/color_question.json +++ b/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/color_question.json @@ -17,6 +17,14 @@ "veryImportantFieldButOnlySometimes": { "title": "Very important field", "type": "string" + }, + "building": { + "properties": { + "floor": { + "title": "Floor", + "type": "number" + } + } } } } diff --git a/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/dynamic_enums_ask_for_color.bpmn b/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/dynamic_enums_ask_for_color.bpmn index 7ec50272..d4f1aa5d 100644 --- a/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/dynamic_enums_ask_for_color.bpmn +++ b/spiffworkflow-backend/tests/data/dynamic_enum_select_fields/dynamic_enums_ask_for_color.bpmn @@ -14,7 +14,7 @@ Flow_1my9ag5 Flow_0b04rbg awesome_color_options = [{"value": "blue", "label": "Blue"}, {"value": "green", "label": "Green"}] -form_ui_hidden_fields = ["veryImportantFieldButOnlySometimes"] +form_ui_hidden_fields = ["veryImportantFieldButOnlySometimes", "building.floor"] diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index 91246c31..c8fd5f6a 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -1686,8 +1686,13 @@ class TestProcessApi(BaseTest): response.json["form_schema"]["definitions"]["Color"]["anyOf"][1]["title"] == "Green" ) + + # if you set this in task data: + # form_ui_hidden_fields = ["veryImportantFieldButOnlySometimes", "building.floor"] + # you will get this ui schema: assert response.json["form_ui_schema"] == { - "veryImportantFieldButOnlySometimes": {"ui:widget": "hidden"} + "building": {"floor": {"ui:widget": "hidden"}}, + "veryImportantFieldButOnlySometimes": {"ui:widget": "hidden"}, } def test_process_instance_list_with_default_list( From 53d99c49d193aa66b46146a532858d9b163a0515 Mon Sep 17 00:00:00 2001 From: burnettk Date: Fri, 3 Feb 2023 16:17:36 -0500 Subject: [PATCH 3/3] refactor some stuff in task_show to separate functions --- .../routes/tasks_controller.py | 99 ++++++++++--------- 1 file changed, 55 insertions(+), 44 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index 086f7a45..72541ceb 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -170,6 +170,25 @@ def task_list_for_my_groups( ) +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 = {} + + if task.data and "form_ui_hidden_fields" in task.data: + hidden_fields = task.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 + 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] = {} + relevant_depth_of_ui_schema = relevant_depth_of_ui_schema[ + hidden_field_part + ] + if len(hidden_field_parts) == ii + 1: + relevant_depth_of_ui_schema["ui:widget"] = "hidden" + + def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response: """Task_show.""" process_instance = _find_process_instance_by_id_or_raise(process_instance_id) @@ -185,20 +204,7 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response process_instance.process_model_identifier, ) - human_task = HumanTaskModel.query.filter_by( - process_instance_id=process_instance_id, task_id=task_id - ).first() - if human_task is None: - raise ( - ApiError( - error_code="no_human_task", - message=( - f"Cannot find a task to complete for task id '{task_id}' and" - f" process instance {process_instance_id}." - ), - status_code=500, - ) - ) + _find_human_task_or_raise(process_instance_id, task_id) form_schema_file_name = "" form_ui_schema_file_name = "" @@ -274,22 +280,7 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response if ui_form_contents: task.form_ui_schema = ui_form_contents - if task.form_ui_schema is None: - task.form_ui_schema = {} - - if task.data and "form_ui_hidden_fields" in task.data: - hidden_fields = task.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 - 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] = {} - relevant_depth_of_ui_schema = relevant_depth_of_ui_schema[ - hidden_field_part - ] - if len(hidden_field_parts) == ii + 1: - relevant_depth_of_ui_schema["ui:widget"] = "hidden" + _munge_form_ui_schema_based_on_hidden_fields_in_task_data(task) if task.properties and task.data and "instructionsForEndUser" in task.properties: if task.properties["instructionsForEndUser"]: @@ -367,20 +358,11 @@ def task_submit_shared( if terminate_loop and spiff_task.is_looping(): spiff_task.terminate_loop() - human_task = HumanTaskModel.query.filter_by( - process_instance_id=process_instance_id, task_id=task_id, completed=False - ).first() - if human_task is None: - raise ( - ApiError( - error_code="no_human_task", - message=( - f"Cannot find a task to complete for task id '{task_id}' and" - f" process instance {process_instance_id}." - ), - status_code=500, - ) - ) + human_task = _find_human_task_or_raise( + process_instance_id=process_instance_id, + task_id=task_id, + only_tasks_that_can_be_completed=True, + ) with sentry_sdk.start_span(op="task", description="complete_form_task"): processor.lock_process_instance("Web") @@ -685,3 +667,32 @@ def _get_potential_owner_usernames(assigned_user: AliasedClass) -> Any: ).label("potential_owner_usernames") return potential_owner_usernames_from_group_concat_or_similar + + +def _find_human_task_or_raise( + process_instance_id: int, + task_id: str, + only_tasks_that_can_be_completed: bool = False, +) -> HumanTaskModel: + if only_tasks_that_can_be_completed: + human_task_query = HumanTaskModel.query.filter_by( + process_instance_id=process_instance_id, task_id=task_id, completed=False + ) + else: + human_task_query = HumanTaskModel.query.filter_by( + process_instance_id=process_instance_id, task_id=task_id + ) + + human_task: HumanTaskModel = human_task_query.first() + if human_task is None: + raise ( + ApiError( + error_code="no_human_task", + message=( + f"Cannot find a task to complete for task id '{task_id}' and" + f" process instance {process_instance_id}." + ), + status_code=500, + ) + ) + return human_task