diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index f7fa3f036..6fa28040e 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1590,7 +1590,7 @@ paths: - name: task_guid in: path required: true - description: The guid of the task to show. + description: The unique id of the task. schema: type: string get: @@ -1605,35 +1605,8 @@ paths: application/json: schema: $ref: "#/components/schemas/Task" - - /task-data/{modified_process_model_identifier}/{process_instance_id}/{task_id}: - parameters: - - name: modified_process_model_identifier - in: path - required: true - description: The modified id of an existing process model - schema: - type: string - - name: process_instance_id - in: path - required: true - description: The unique id of an existing process instance. - schema: - type: integer - - name: task_id - in: path - required: true - description: The unique id of the task. - schema: - type: string - - name: task_guid - in: path - required: true - description: The guid of the task to show. - schema: - type: string put: - operationId: spiffworkflow_backend.routes.process_api_blueprint.task_data_update + operationId: spiffworkflow_backend.routes.tasks_controller.task_data_update summary: Update the task data for requested instance and task tags: - Process Instances @@ -1738,7 +1711,7 @@ paths: schema: $ref: "#/components/schemas/Workflow" - /task-complete/{modified_process_model_identifier}/{process_instance_id}/{task_id}: + /task-complete/{modified_process_model_identifier}/{process_instance_id}/{task_guid}: parameters: - name: modified_process_model_identifier in: path @@ -1752,14 +1725,14 @@ paths: description: The unique id of the process instance schema: type: string - - name: task_id + - name: task_guid in: path required: true description: The unique id of the task. schema: type: string post: - operationId: spiffworkflow_backend.routes.process_api_blueprint.manual_complete_task + operationId: spiffworkflow_backend.routes.tasks_controller.manual_complete_task summary: Mark a task complete without executing it tags: - Process Instances @@ -1838,9 +1811,9 @@ paths: schema: $ref: "#/components/schemas/ServiceTask" - /tasks/{process_instance_id}/{task_id}: + /tasks/{process_instance_id}/{task_guid}: parameters: - - name: task_id + - name: task_guid in: path required: true description: The unique id of an existing process group. diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index a07f5f494..ac38eff02 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -16,15 +16,9 @@ from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( ProcessEntityNotFoundError, ) -from spiffworkflow_backend.models.db import db -from spiffworkflow_backend.models.json_data import JsonDataModel from spiffworkflow_backend.models.principal import PrincipalModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema -from spiffworkflow_backend.models.process_instance import ( - ProcessInstanceTaskDataCannotBeUpdatedError, -) -from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType from spiffworkflow_backend.models.process_instance_file_data import ( ProcessInstanceFileDataModel, ) @@ -38,7 +32,6 @@ from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) from spiffworkflow_backend.services.process_model_service import ProcessModelService -from spiffworkflow_backend.services.task_service import TaskService process_api_blueprint = Blueprint("process_api", __name__) @@ -169,60 +162,6 @@ def github_webhook_receive(body: Dict) -> Response: return Response(json.dumps({"git_pull": result}), status=200, mimetype="application/json") -def task_data_update( - process_instance_id: str, - modified_process_model_identifier: str, - task_id: str, - body: Dict, -) -> Response: - """Update task data.""" - process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first() - if process_instance: - if process_instance.status != "suspended": - raise ProcessInstanceTaskDataCannotBeUpdatedError( - "The process instance needs to be suspended to update the task-data." - f" It is currently: {process_instance.status}" - ) - - task_model = TaskModel.query.filter_by(guid=task_id).first() - if task_model is None: - raise ApiError( - error_code="update_task_data_error", - message=f"Could not find Task: {task_id} in Instance: {process_instance_id}.", - ) - - if "new_task_data" in body: - new_task_data_str: str = body["new_task_data"] - new_task_data_dict = json.loads(new_task_data_str) - json_data_dict = TaskService.update_task_data_on_task_model( - task_model, new_task_data_dict, "json_data_hash" - ) - if json_data_dict is not None: - json_data = JsonDataModel(**json_data_dict) - db.session.add(json_data) - ProcessInstanceProcessor.add_event_to_process_instance( - process_instance, ProcessInstanceEventType.task_data_edited.value, task_guid=task_id - ) - try: - db.session.commit() - except Exception as e: - db.session.rollback() - raise ApiError( - error_code="update_task_data_error", - message=f"Could not update the Instance. Original error is {e}", - ) from e - else: - raise ApiError( - error_code="update_task_data_error", - message=f"Could not update task data for Instance: {process_instance_id}, and Task: {task_id}.", - ) - return Response( - json.dumps(ProcessInstanceModelSchema().dump(process_instance)), - status=200, - mimetype="application/json", - ) - - def _get_required_parameter_or_raise(parameter: str, post_body: dict[str, Any]) -> Any: """Get_required_parameter_or_raise.""" return_value = None @@ -263,30 +202,6 @@ def send_bpmn_event( ) -def manual_complete_task( - modified_process_model_identifier: str, - process_instance_id: str, - task_id: str, - body: Dict, -) -> Response: - """Mark a task complete without executing it.""" - execute = body.get("execute", True) - process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first() - if process_instance: - processor = ProcessInstanceProcessor(process_instance) - processor.manual_complete_task(task_id, execute) - else: - raise ApiError( - error_code="complete_task", - message=f"Could not complete Task {task_id} in Instance {process_instance_id}", - ) - return Response( - json.dumps(ProcessInstanceModelSchema().dump(process_instance)), - status=200, - mimetype="application/json", - ) - - def _commit_and_push_to_git(message: str) -> None: """Commit_and_push_to_git.""" if current_app.config["SPIFFWORKFLOW_BACKEND_GIT_COMMIT_ON_SAVE"]: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py index bb5cef6c2..48a931c21 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -218,6 +218,7 @@ def process_instance_resume( try: processor.lock_process_instance("Web") processor.resume() + processor.do_engine_steps(save=True) except (ProcessInstanceIsNotEnqueuedError, ProcessInstanceIsAlreadyLockedError) as e: ErrorHandlingService().handle_error(processor, e) raise e diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index 495d22dec..c49eda582 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -33,8 +33,14 @@ from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.human_task import HumanTaskModel from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel +from spiffworkflow_backend.models.json_data import JsonDataModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel +from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus +from spiffworkflow_backend.models.process_instance import ( + ProcessInstanceTaskDataCannotBeUpdatedError, +) +from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.task import Task from spiffworkflow_backend.models.task import TaskModel # noqa: F401 @@ -56,6 +62,7 @@ from spiffworkflow_backend.services.process_instance_service import ( ) from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.spec_file_service import SpecFileService +from spiffworkflow_backend.services.task_service import TaskService class TaskDataSelectOption(TypedDict): @@ -182,24 +189,85 @@ def task_data_show( return make_response(jsonify(task_model), 200) -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 task_data_update( + process_instance_id: str, + modified_process_model_identifier: str, + task_guid: str, + body: Dict, +) -> Response: + """Update task data.""" + process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first() + if process_instance: + if process_instance.status != "suspended": + raise ProcessInstanceTaskDataCannotBeUpdatedError( + "The process instance needs to be suspended to update the task-data." + f" It is currently: {process_instance.status}" + ) - 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" + task_model = TaskModel.query.filter_by(guid=task_guid).first() + if task_model is None: + raise ApiError( + error_code="update_task_data_error", + message=f"Could not find Task: {task_guid} in Instance: {process_instance_id}.", + ) + + if "new_task_data" in body: + new_task_data_str: str = body["new_task_data"] + new_task_data_dict = json.loads(new_task_data_str) + json_data_dict = TaskService.update_task_data_on_task_model( + task_model, new_task_data_dict, "json_data_hash" + ) + if json_data_dict is not None: + json_data = JsonDataModel(**json_data_dict) + db.session.add(json_data) + ProcessInstanceProcessor.add_event_to_process_instance( + process_instance, ProcessInstanceEventType.task_data_edited.value, task_guid=task_guid + ) + try: + db.session.commit() + except Exception as e: + db.session.rollback() + raise ApiError( + error_code="update_task_data_error", + message=f"Could not update the Instance. Original error is {e}", + ) from e + else: + raise ApiError( + error_code="update_task_data_error", + message=f"Could not update task data for Instance: {process_instance_id}, and Task: {task_guid}.", + ) + return Response( + json.dumps(ProcessInstanceModelSchema().dump(process_instance)), + status=200, + mimetype="application/json", + ) -def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response: +def manual_complete_task( + modified_process_model_identifier: str, + process_instance_id: str, + task_guid: str, + body: Dict, +) -> Response: + """Mark a task complete without executing it.""" + execute = body.get("execute", True) + process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first() + if process_instance: + processor = ProcessInstanceProcessor(process_instance) + processor.manual_complete_task(task_guid, execute) + else: + raise ApiError( + error_code="complete_task", + message=f"Could not complete Task {task_guid} in Instance {process_instance_id}", + ) + return Response( + json.dumps(ProcessInstanceModelSchema().dump(process_instance)), + status=200, + mimetype="application/json", + ) + + +def task_show(process_instance_id: int, task_guid: str) -> flask.wrappers.Response: """Task_show.""" process_instance = _find_process_instance_by_id_or_raise(process_instance_id) @@ -214,12 +282,12 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response process_instance.process_model_identifier, ) - _find_human_task_or_raise(process_instance_id, task_id) + _find_human_task_or_raise(process_instance_id, task_guid) form_schema_file_name = "" form_ui_schema_file_name = "" processor = ProcessInstanceProcessor(process_instance) - spiff_task = _get_spiff_task_from_process_instance(task_id, process_instance, processor=processor) + spiff_task = _get_spiff_task_from_process_instance(task_guid, process_instance, processor=processor) extensions = spiff_task.task_spec.extensions if "properties" in extensions: @@ -252,7 +320,8 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response ApiError( error_code="missing_form_file", message=( - f"Cannot find a form file for process_instance_id: {process_instance_id}, task_id: {task_id}" + f"Cannot find a form file for process_instance_id: {process_instance_id}, task_guid:" + f" {task_guid}" ), status_code=400, ) @@ -319,7 +388,7 @@ def process_data_show( def task_submit_shared( process_instance_id: int, - task_id: str, + task_guid: str, body: Dict[str, Any], terminate_loop: bool = False, ) -> flask.wrappers.Response: @@ -336,7 +405,7 @@ def task_submit_shared( ) processor = ProcessInstanceProcessor(process_instance) - spiff_task = _get_spiff_task_from_process_instance(task_id, 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) if spiff_task.state != TaskState.READY: @@ -353,7 +422,7 @@ def task_submit_shared( human_task = _find_human_task_or_raise( process_instance_id=process_instance_id, - task_id=task_id, + task_guid=task_guid, only_tasks_that_can_be_completed=True, ) @@ -398,13 +467,13 @@ def task_submit_shared( def task_submit( process_instance_id: int, - task_id: str, + task_guid: str, body: Dict[str, Any], terminate_loop: bool = False, ) -> flask.wrappers.Response: """Task_submit_user_data.""" with sentry_sdk.start_span(op="controller_action", description="tasks_controller.task_submit"): - return task_submit_shared(process_instance_id, task_id, body, terminate_loop) + return task_submit_shared(process_instance_id, task_guid, body, terminate_loop) def _get_tasks( @@ -559,14 +628,14 @@ def _render_jinja_template(unprocessed_template: str, spiff_task: SpiffTask) -> def _get_spiff_task_from_process_instance( - task_id: str, + task_guid: str, process_instance: ProcessInstanceModel, processor: Union[ProcessInstanceProcessor, None] = None, ) -> SpiffTask: """Get_spiff_task_from_process_instance.""" if processor is None: processor = ProcessInstanceProcessor(process_instance) - task_uuid = uuid.UUID(task_id) + task_uuid = uuid.UUID(task_guid) spiff_task = processor.bpmn_process_instance.get_task(task_uuid) if spiff_task is None: @@ -658,15 +727,15 @@ def _get_potential_owner_usernames(assigned_user: AliasedClass) -> Any: def _find_human_task_or_raise( process_instance_id: int, - task_id: str, + task_guid: 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 + process_instance_id=process_instance_id, task_id=task_guid, completed=False ) else: - human_task_query = HumanTaskModel.query.filter_by(process_instance_id=process_instance_id, task_id=task_id) + human_task_query = HumanTaskModel.query.filter_by(process_instance_id=process_instance_id, task_id=task_guid) human_task: HumanTaskModel = human_task_query.first() if human_task is None: @@ -674,10 +743,27 @@ def _find_human_task_or_raise( ApiError( error_code="no_human_task", message=( - f"Cannot find a task to complete for task id '{task_id}' and" + f"Cannot find a task to complete for task id '{task_guid}' and" f" process instance {process_instance_id}." ), status_code=500, ) ) 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 = {} + + 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" diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx index 07cb45fdf..308f4bd1e 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceShow.tsx @@ -234,10 +234,11 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { return params.to_task_guid; }; - const showingLastSpiffStep = () => { - return ( - processInstance && currentToTaskGuid() === processInstance.spiff_step - ); + // right now this just assume if to_task_guid was passed in then + // this cannot be the active task. + // we may need a better way to figure this out. + const showingActiveTask = () => { + return !params.to_task_guid; }; const completionViewLink = (label: any, taskGuid: string) => { @@ -496,7 +497,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { const initializeTaskDataToDisplay = (task: Task | null) => { if ( task && - task.state === 'COMPLETED' && + (task.state === 'COMPLETED' || task.state === 'READY') && ability.can('GET', targetUris.processInstanceTaskDataPath) ) { setShowTaskDataLoading(true); @@ -624,7 +625,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { ability.can('PUT', targetUris.processInstanceTaskDataPath) && isActiveTask(task) && processInstance.status === 'suspended' && - showingLastSpiffStep() + showingActiveTask() ); }; @@ -637,7 +638,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { ability.can('POST', targetUris.processInstanceSendEventPath) && taskTypes.filter((t) => t === task.typename).length > 0 && task.state === 'WAITING' && - showingLastSpiffStep() + showingActiveTask() ); }; @@ -647,18 +648,20 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { processInstance.status === 'suspended' && ability.can('POST', targetUris.processInstanceCompleteTaskPath) && isActiveTask(task) && - showingLastSpiffStep() + showingActiveTask() ); }; - const canResetProcess = (task: Task) => { - return ( - ability.can('POST', targetUris.processInstanceResetPath) && - processInstance && - processInstance.status === 'suspended' && - task.state === 'READY' && - !showingLastSpiffStep() - ); + const canResetProcess = (_task: Task) => { + // disabling this feature for now + return false; + // return ( + // ability.can('POST', targetUris.processInstanceResetPath) && + // processInstance && + // processInstance.status === 'suspended' && + // task.state === 'READY' && + // !showingActiveTask() + // ); }; const getEvents = (task: Task) => { @@ -714,7 +717,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { // taskToUse is copy of taskToDisplay, with taskDataToDisplay in data attribute const taskToUse: Task = { ...taskToDisplay, data: taskDataToDisplay }; HttpService.makeCallToBackend({ - path: `${targetUris.processInstanceTaskDataPath}/${taskToUse.id}`, + path: `${targetUris.processInstanceTaskDataPath}/${taskToUse.guid}`, httpMethod: 'PUT', successCallback: saveTaskDataResult, failureCallback: addError, @@ -976,7 +979,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { ): {taskToUse.state} {taskDisplayButtons(taskToUse)} - {taskToUse.state == 'COMPLETED' ? ( + {taskToUse.state === 'COMPLETED' ? (