From 9049a64925c43a8ebbcb48ae644db7736f9daa07 Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 6 Jan 2023 15:50:47 -0500 Subject: [PATCH] actually filter by process initiator w/ burnettk --- .../src/spiffworkflow_backend/api.yml | 12 +++ .../routes/process_instances_controller.py | 4 + .../process_instance_report_service.py | 61 ++++++++++--- .../integration/test_process_api.py | 89 +++++++++++++++++++ .../ProcessInstanceListSaveAsReport.tsx | 10 +++ .../components/ProcessInstanceListTable.tsx | 59 ++++++++---- 6 files changed, 205 insertions(+), 30 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 6c720265c..1055ba431 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -628,6 +628,12 @@ paths: description: The identifier of the group to get the process instances for schema: type: string + - name: process_initiator_username + in: query + required: false + description: The username of the process initiator + schema: + type: string get: operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_list_for_me summary: Returns a list of process instances that are associated with me. @@ -741,6 +747,12 @@ paths: description: The identifier of the group to get the process instances for schema: type: string + - name: process_initiator_username + in: query + required: false + description: The username of the process initiator + schema: + type: string get: operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_list summary: Returns a list of process instances. 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 31cd66a33..a67091534 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -219,6 +219,7 @@ def process_instance_list_for_me( report_identifier: Optional[str] = None, report_id: Optional[int] = None, user_group_identifier: Optional[str] = None, + process_initiator_username: Optional[str] = None, ) -> flask.wrappers.Response: """Process_instance_list_for_me.""" return process_instance_list( @@ -252,6 +253,7 @@ def process_instance_list( report_identifier: Optional[str] = None, report_id: Optional[int] = None, user_group_identifier: Optional[str] = None, + process_initiator_username: Optional[str] = None, ) -> flask.wrappers.Response: """Process_instance_list.""" process_instance_report = ProcessInstanceReportService.report_with_identifier( @@ -268,6 +270,7 @@ def process_instance_list( end_to=end_to, with_relation_to_me=with_relation_to_me, process_status=process_status.split(",") if process_status else None, + process_initiator_username=process_initiator_username, ) else: report_filter = ( @@ -281,6 +284,7 @@ def process_instance_list( end_to=end_to, process_status=process_status, with_relation_to_me=with_relation_to_me, + process_initiator_username=process_initiator_username, ) ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index cd20b9b57..47156d3fb 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -28,6 +28,10 @@ from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignme from spiffworkflow_backend.services.process_model_service import ProcessModelService +class ProcessInstanceReportNotFoundError(Exception): + """ProcessInstanceReportNotFoundError.""" + + @dataclass class ProcessInstanceReportFilter: """ProcessInstanceReportFilter.""" @@ -44,6 +48,7 @@ class ProcessInstanceReportFilter: with_tasks_completed_by_me: Optional[bool] = None with_tasks_assigned_to_my_group: Optional[bool] = None with_relation_to_me: Optional[bool] = None + process_initiator_username: Optional[str] = (None,) def to_dict(self) -> dict[str, str]: """To_dict.""" @@ -77,6 +82,8 @@ class ProcessInstanceReportFilter: ).lower() if self.with_relation_to_me is not None: d["with_relation_to_me"] = str(self.with_relation_to_me).lower() + if self.process_initiator_username is not None: + d["process_initiator_username"] = str(self.process_initiator_username) return d @@ -85,7 +92,7 @@ class ProcessInstanceReportService: """ProcessInstanceReportService.""" @classmethod - def system_metadata_map(cls, metadata_key: str) -> dict[str, Any]: + def system_metadata_map(cls, metadata_key: str) -> Optional[dict[str, Any]]: """System_metadata_map.""" # TODO replace with system reports that are loaded on launch (or similar) temp_system_metadata_map = { @@ -131,6 +138,9 @@ class ProcessInstanceReportService: "order_by": ["-start_in_seconds", "-id"], }, } + + if metadata_key not in temp_system_metadata_map: + return None return temp_system_metadata_map[metadata_key] @classmethod @@ -157,10 +167,17 @@ class ProcessInstanceReportService: if process_instance_report is not None: return process_instance_report # type: ignore + report_metadata = cls.system_metadata_map(report_identifier) + if report_metadata is None: + raise ProcessInstanceReportNotFoundError( + f"Could not find a report with identifier '{report_identifier}' for" + f" user '{user.username}'" + ) + process_instance_report = ProcessInstanceReportModel( identifier=report_identifier, created_by_id=user.id, - report_metadata=cls.system_metadata_map(report_identifier), + report_metadata=report_metadata, ) return process_instance_report # type: ignore @@ -210,20 +227,22 @@ class ProcessInstanceReportService: with_tasks_completed_by_me = bool_value("with_tasks_completed_by_me") with_tasks_assigned_to_my_group = bool_value("with_tasks_assigned_to_my_group") with_relation_to_me = bool_value("with_relation_to_me") + process_initiator_username = filters.get("process_initiator_username") report_filter = ProcessInstanceReportFilter( - process_model_identifier, - user_group_identifier, - start_from, - start_to, - end_from, - end_to, - process_status, - initiated_by_me, - has_terminal_status, - with_tasks_completed_by_me, - with_tasks_assigned_to_my_group, - with_relation_to_me, + process_model_identifier=process_model_identifier, + user_group_identifier=user_group_identifier, + start_from=start_from, + start_to=start_to, + end_from=end_from, + end_to=end_to, + process_status=process_status, + initiated_by_me=initiated_by_me, + has_terminal_status=has_terminal_status, + with_tasks_completed_by_me=with_tasks_completed_by_me, + with_tasks_assigned_to_my_group=with_tasks_assigned_to_my_group, + with_relation_to_me=with_relation_to_me, + process_initiator_username=process_initiator_username, ) return report_filter @@ -244,6 +263,7 @@ class ProcessInstanceReportService: with_tasks_completed_by_me: Optional[bool] = None, with_tasks_assigned_to_my_group: Optional[bool] = None, with_relation_to_me: Optional[bool] = None, + process_initiator_username: Optional[str] = None, ) -> ProcessInstanceReportFilter: """Filter_from_metadata_with_overrides.""" report_filter = cls.filter_from_metadata(process_instance_report) @@ -268,6 +288,8 @@ class ProcessInstanceReportService: report_filter.has_terminal_status = has_terminal_status if with_tasks_completed_by_me is not None: report_filter.with_tasks_completed_by_me = with_tasks_completed_by_me + if process_initiator_username is not None: + report_filter.process_initiator_username = process_initiator_username if with_tasks_assigned_to_my_group is not None: report_filter.with_tasks_assigned_to_my_group = ( with_tasks_assigned_to_my_group @@ -386,6 +408,17 @@ class ProcessInstanceReportService: ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore ) + if report_filter.process_initiator_username is not None: + user = UserModel.query.filter_by( + username=report_filter.process_initiator_username + ).first() + process_initiator_id = -1 + if user: + process_initiator_id = user.id + process_instance_query = process_instance_query.filter_by( + process_initiator_id=process_initiator_id + ) + if ( not report_filter.with_tasks_completed_by_me and not report_filter.with_tasks_assigned_to_my_group 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 ef34fe060..b7b3da251 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -3046,6 +3046,95 @@ class TestProcessApi(BaseTest): assert response.json["pagination"]["pages"] == 1 assert response.json["pagination"]["total"] == 1 + def test_can_get_process_instance_list_with_report_metadata_and_process_initator( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_can_get_process_instance_list_with_report_metadata_and_process_initator.""" + user_one = self.create_user_with_permission(username="user_one") + + process_model = load_test_spec( + process_model_id=( + "save_process_instance_metadata/save_process_instance_metadata" + ), + bpmn_file_name="save_process_instance_metadata.bpmn", + process_model_source_directory="save_process_instance_metadata", + ) + self.create_process_instance_from_process_model( + process_model=process_model, user=user_one + ) + self.create_process_instance_from_process_model( + process_model=process_model, user=user_one + ) + self.create_process_instance_from_process_model( + process_model=process_model, user=with_super_admin_user + ) + + dne_report_metadata = { + "columns": [ + {"Header": "ID", "accessor": "id"}, + {"Header": "Status", "accessor": "status"}, + {"Header": "Process Initiator", "accessor": "username"}, + ], + "order_by": ["status"], + "filter_by": [ + { + "field_name": "process_initiator_username", + "field_value": "DNE", + "operator": "equals", + } + ], + } + + user_one_report_metadata = { + "columns": [ + {"Header": "ID", "accessor": "id"}, + {"Header": "Status", "accessor": "status"}, + {"Header": "Process Initiator", "accessor": "username"}, + ], + "order_by": ["status"], + "filter_by": [ + { + "field_name": "process_initiator_username", + "field_value": user_one.username, + "operator": "equals", + } + ], + } + process_instance_report_dne = ProcessInstanceReportModel.create_with_attributes( + identifier="dne_report", + report_metadata=dne_report_metadata, + user=user_one, + ) + process_instance_report_user_one = ( + ProcessInstanceReportModel.create_with_attributes( + identifier="user_one_report", + report_metadata=user_one_report_metadata, + user=user_one, + ) + ) + + response = client.get( + f"/v1.0/process-instances?report_identifier={process_instance_report_user_one.identifier}", + headers=self.logged_in_headers(user_one), + ) + assert response.json is not None + assert response.status_code == 200 + assert len(response.json["results"]) == 2 + assert response.json["results"][0]["username"] == user_one.username + assert response.json["results"][1]["username"] == user_one.username + + response = client.get( + f"/v1.0/process-instances?report_identifier={process_instance_report_dne.identifier}", + headers=self.logged_in_headers(user_one), + ) + assert response.json is not None + assert response.status_code == 200 + assert len(response.json["results"]) == 0 + def test_can_get_process_instance_report_column_list( self, app: Flask, diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx index a3d50d94b..6953a20cb 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx @@ -12,6 +12,7 @@ import { ProcessModel, ReportColumn, ReportMetadata, + User, } from '../interfaces'; import HttpService from '../services/HttpService'; @@ -20,6 +21,7 @@ type OwnProps = { columnArray: ReportColumn[]; orderBy: string; processModelSelection: ProcessModel | null; + processInitiatorSelection: User | null; processStatusSelection: string[]; startFromSeconds: string | null; startToSeconds: string | null; @@ -36,6 +38,7 @@ export default function ProcessInstanceListSaveAsReport({ columnArray, orderBy, processModelSelection, + processInitiatorSelection, processInstanceReportSelection, processStatusSelection, startFromSeconds, @@ -86,6 +89,13 @@ export default function ProcessInstanceListSaveAsReport({ }); } + if (processInitiatorSelection) { + filterByArray.push({ + field_name: 'process_initiator_username', + field_value: processInitiatorSelection.username, + }); + } + if (processStatusSelection.length > 0) { filterByArray.push({ field_name: 'process_status', diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 51e8c2d6a..b161d58de 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -193,11 +193,42 @@ export default function ProcessInstanceListTable({ setEndToTime, ]); + const handleProcessInstanceInitiatorSearchResult = ( + result: any, + inputText: string + ) => { + if (lastRequestedInitatorSearchTerm.current === result.username_prefix) { + setProcessInstanceInitiatorOptions(result.users); + result.users.forEach((user: User) => { + if (user.username === inputText) { + setProcessInitiatorSelection(user); + } + }); + } + }; + + const searchForProcessInitiator = (inputText: string) => { + if (inputText) { + lastRequestedInitatorSearchTerm.current = inputText; + HttpService.makeCallToBackend({ + path: `/users/search?username_prefix=${inputText}`, + successCallback: (result: any) => + handleProcessInstanceInitiatorSearchResult(result, inputText), + }); + } + }; + const parametersToGetFromSearchParams = useMemo(() => { + const figureOutProcessInitiator = (processInitiatorSearchText: string) => { + searchForProcessInitiator(processInitiatorSearchText); + }; + return { process_model_identifier: null, process_status: null, + process_initiator_username: figureOutProcessInitiator, }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // eslint-disable-next-line sonarjs/cognitive-complexity @@ -384,6 +415,12 @@ export default function ProcessInstanceListTable({ } }); + if (filters.process_initiator_username) { + const functionToCall = + parametersToGetFromSearchParams.process_initiator_username; + functionToCall(filters.process_initiator_username); + } + const processStatusSelectedArray: string[] = []; if (filters.process_status) { PROCESS_STATUSES.forEach((processStatusOption: any) => { @@ -538,8 +575,13 @@ export default function ProcessInstanceListTable({ queryParamString += `&report_id=${processInstanceReportSelection.id}`; } + if (processInitiatorSelection) { + queryParamString += `&process_initiator_username=${processInitiatorSelection.username}`; + } + setErrorObject(null); setProcessInstanceReportJustSaved(null); + setProcessInstanceFilters({}); navigate(`${processInstanceListPathPrefix}?${queryParamString}`); }; @@ -682,6 +724,7 @@ export default function ProcessInstanceListTable({ orderBy="" buttonText="Save" processModelSelection={processModelSelection} + processInitiatorSelection={processInitiatorSelection} processStatusSelection={processStatusSelection} processInstanceReportSelection={processInstanceReportSelection} reportMetadata={reportMetadata} @@ -987,22 +1030,6 @@ export default function ProcessInstanceListTable({ return null; }; - const handleProcessInstanceInitiatorSearchResult = (result: any) => { - if (lastRequestedInitatorSearchTerm.current === result.username_prefix) { - setProcessInstanceInitiatorOptions(result.users); - } - }; - - const searchForProcessInitiator = (inputText: string) => { - if (inputText) { - lastRequestedInitatorSearchTerm.current = inputText; - HttpService.makeCallToBackend({ - path: `/users/search?username_prefix=${inputText}`, - successCallback: handleProcessInstanceInitiatorSearchResult, - }); - } - }; - const filterOptions = () => { if (!showFilterOptions) { return null;