From 8b562b0bf2b561b95affeecbb40477203d8ead86 Mon Sep 17 00:00:00 2001 From: jbirddog <100367399+jbirddog@users.noreply.github.com> Date: Tue, 22 Nov 2022 17:05:58 -0500 Subject: [PATCH 1/2] Update oauth redirect url (#58) --- .../src/spiffworkflow_backend/routes/process_api_blueprint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9026abb0e..f11358438 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -1005,7 +1005,7 @@ def authentication_callback( f"{service}/{auth_method}", response, g.user.id, create_if_not_exists=True ) return redirect( - f"{current_app.config['SPIFFWORKFLOW_FRONTEND_URL']}/admin/authentications" + f"{current_app.config['SPIFFWORKFLOW_FRONTEND_URL']}/admin/configuration" ) From ceefa9a2f7711f40f4c7126c151a710c33d9a7b0 Mon Sep 17 00:00:00 2001 From: jbirddog <100367399+jbirddog@users.noreply.github.com> Date: Tue, 22 Nov 2022 17:14:51 -0500 Subject: [PATCH 2/2] Start of system report filters (#57) --- .../src/spiffworkflow_backend/api.yml | 18 ++++ .../routes/process_api_blueprint.py | 87 ++++++++++++++++++- .../process_instance_report_service.py | 58 +++++++++++-- .../src/components/MyCompletedInstances.tsx | 2 + .../components/ProcessInstanceListTable.tsx | 14 ++- .../src/routes/CompletedInstances.tsx | 31 ++++++- 6 files changed, 196 insertions(+), 14 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index e5e0db974..f85cc74fe 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -514,6 +514,24 @@ paths: description: For filtering - not_started, user_input_required, waiting, complete, error, or suspended schema: type: string + - name: initiated_by_me + in: query + required: false + description: For filtering - show instances initiated by me + schema: + type: boolean + - name: with_tasks_completed_by_me + in: query + required: false + description: For filtering - show instances with tasks completed by me + schema: + type: boolean + - name: with_tasks_completed_by_my_group + in: query + required: false + description: For filtering - show instances with tasks completed by my group + schema: + type: boolean - name: user_filter in: query required: false 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 f11358438..31eff335b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -30,6 +30,7 @@ from SpiffWorkflow.task import TaskState from sqlalchemy import and_ from sqlalchemy import asc from sqlalchemy import desc +from sqlalchemy import select from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( ProcessEntityNotFoundError, @@ -63,6 +64,7 @@ from spiffworkflow_backend.models.spec_reference import SpecReferenceSchema from spiffworkflow_backend.models.spiff_logging import SpiffLoggingModel from spiffworkflow_backend.models.spiff_step_details import SpiffStepDetailsModel from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel from spiffworkflow_backend.routes.user import verify_token from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService @@ -762,6 +764,9 @@ def process_instance_list( end_from: Optional[int] = None, end_to: Optional[int] = None, process_status: Optional[str] = None, + initiated_by_me: Optional[bool] = None, + with_tasks_completed_by_me: Optional[bool] = None, + with_tasks_completed_by_my_group: Optional[bool] = None, user_filter: Optional[bool] = False, report_identifier: Optional[str] = None, ) -> flask.wrappers.Response: @@ -778,6 +783,9 @@ def process_instance_list( end_from, end_to, process_status.split(",") if process_status else None, + initiated_by_me, + with_tasks_completed_by_me, + with_tasks_completed_by_my_group, ) else: report_filter = ( @@ -789,6 +797,9 @@ def process_instance_list( end_from, end_to, process_status, + initiated_by_me, + with_tasks_completed_by_me, + with_tasks_completed_by_my_group, ) ) @@ -837,9 +848,79 @@ def process_instance_list( ProcessInstanceModel.status.in_(report_filter.process_status) # type: ignore ) - process_instances = process_instance_query.order_by( - ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore - ).paginate(page=page, per_page=per_page, error_out=False) + if report_filter.initiated_by_me is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status == "complete" + ) + process_instance_query = process_instance_query.filter_by( + process_initiator=g.user + ) + + # TODO: not sure if this is exactly what is wanted + if report_filter.with_tasks_completed_by_me is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status == "complete" + ) + process_instance_query = process_instance_query.join( + SpiffStepDetailsModel, + ProcessInstanceModel.id == SpiffStepDetailsModel.process_instance_id, + ) + process_instance_query = process_instance_query.join( + SpiffLoggingModel, + ProcessInstanceModel.id == SpiffLoggingModel.process_instance_id, + ) + process_instance_query = process_instance_query.filter( + SpiffLoggingModel.message.contains("COMPLETED") # type: ignore + ) + process_instance_query = process_instance_query.filter( + SpiffLoggingModel.spiff_step == SpiffStepDetailsModel.spiff_step + ) + process_instance_query = process_instance_query.filter( + SpiffStepDetailsModel.completed_by_user_id == g.user.id + ) + + # TODO: not sure if this is exactly what is wanted + if report_filter.with_tasks_completed_by_my_group is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status == "complete" + ) + process_instance_query = process_instance_query.join( + SpiffStepDetailsModel, + ProcessInstanceModel.id == SpiffStepDetailsModel.process_instance_id, + ) + process_instance_query = process_instance_query.join( + SpiffLoggingModel, + ProcessInstanceModel.id == SpiffLoggingModel.process_instance_id, + ) + process_instance_query = process_instance_query.filter( + SpiffLoggingModel.message.contains("COMPLETED") # type: ignore + ) + process_instance_query = process_instance_query.filter( + SpiffLoggingModel.spiff_step == SpiffStepDetailsModel.spiff_step + ) + + my_groups = ( + select(UserGroupAssignmentModel) # type: ignore + .where(UserGroupAssignmentModel.user_id == g.user.id) + .with_only_columns(UserGroupAssignmentModel.group_id) + ) + users_in_my_groups = ( + select(UserGroupAssignmentModel) # type: ignore + .where(UserGroupAssignmentModel.group_id.in_(my_groups)) + .with_only_columns(UserGroupAssignmentModel.user_id) + ) + + process_instance_query = process_instance_query.filter( + SpiffStepDetailsModel.completed_by_user_id.in_(users_in_my_groups) # type: ignore + ) + + process_instances = ( + process_instance_query.distinct() + .order_by( + ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore + ) + .paginate(page=page, per_page=per_page, error_out=False) + ) results = list( map( 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 9c398514b..731abcdbd 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 @@ -18,6 +18,9 @@ class ProcessInstanceReportFilter: end_from: Optional[int] = None end_to: Optional[int] = None process_status: Optional[list[str]] = None + initiated_by_me: Optional[bool] = None + with_tasks_completed_by_me: Optional[bool] = None + with_tasks_completed_by_my_group: Optional[bool] = None def to_dict(self) -> dict[str, str]: """To_dict.""" @@ -35,6 +38,16 @@ class ProcessInstanceReportFilter: d["end_to"] = str(self.end_to) if self.process_status is not None: d["process_status"] = ",".join(self.process_status) + if self.initiated_by_me is not None: + d["initiated_by_me"] = str(self.initiated_by_me).lower() + if self.with_tasks_completed_by_me is not None: + d["with_tasks_completed_by_me"] = str( + self.with_tasks_completed_by_me + ).lower() + if self.with_tasks_completed_by_my_group is not None: + d["with_tasks_completed_by_my_group"] = str( + self.with_tasks_completed_by_my_group + ).lower() return d @@ -73,30 +86,35 @@ class ProcessInstanceReportService: }, "system_report_instances_initiated_by_me": { "columns": [ + {"Header": "id", "accessor": "id"}, { "Header": "process_model_identifier", "accessor": "process_model_identifier", }, {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, - {"Header": "id", "accessor": "id"}, {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, {"Header": "status", "accessor": "status"}, ], + "filter_by": [{"field_name": "initiated_by_me", "field_value": True}], }, "system_report_instances_with_tasks_completed_by_me": { "columns": [ - {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, - {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, - {"Header": "status", "accessor": "status"}, {"Header": "id", "accessor": "id"}, { "Header": "process_model_identifier", "accessor": "process_model_identifier", }, + {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, + {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, + {"Header": "status", "accessor": "status"}, + ], + "filter_by": [ + {"field_name": "with_tasks_completed_by_me", "field_value": True} ], }, "system_report_instances_with_tasks_completed_by_my_groups": { "columns": [ + {"Header": "id", "accessor": "id"}, { "Header": "process_model_identifier", "accessor": "process_model_identifier", @@ -104,7 +122,12 @@ class ProcessInstanceReportService: {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, {"Header": "status", "accessor": "status"}, - {"Header": "id", "accessor": "id"}, + ], + "filter_by": [ + { + "field_name": "with_tasks_completed_by_my_group", + "field_value": True, + } ], }, } @@ -112,7 +135,7 @@ class ProcessInstanceReportService: process_instance_report = ProcessInstanceReportModel( identifier=report_identifier, created_by_id=user.id, - report_metadata=temp_system_metadata_map[report_identifier], + report_metadata=temp_system_metadata_map[report_identifier], # type: ignore ) return process_instance_report # type: ignore @@ -138,6 +161,10 @@ class ProcessInstanceReportService: """Filter_from_metadata.""" filters = cls.filter_by_to_dict(process_instance_report) + def bool_value(key: str) -> Optional[bool]: + """Bool_value.""" + return bool(filters[key]) if key in filters else None + def int_value(key: str) -> Optional[int]: """Int_value.""" return int(filters[key]) if key in filters else None @@ -152,6 +179,11 @@ class ProcessInstanceReportService: end_from = int_value("end_from") end_to = int_value("end_to") process_status = list_value("process_status") + initiated_by_me = bool_value("initiated_by_me") + with_tasks_completed_by_me = bool_value("with_tasks_completed_by_me") + with_tasks_completed_by_my_group = bool_value( + "with_tasks_completed_by_my_group" + ) report_filter = ProcessInstanceReportFilter( process_model_identifier, @@ -160,6 +192,9 @@ class ProcessInstanceReportService: end_from, end_to, process_status, + initiated_by_me, + with_tasks_completed_by_me, + with_tasks_completed_by_my_group, ) return report_filter @@ -174,6 +209,9 @@ class ProcessInstanceReportService: end_from: Optional[int] = None, end_to: Optional[int] = None, process_status: Optional[str] = None, + initiated_by_me: Optional[bool] = None, + with_tasks_completed_by_me: Optional[bool] = None, + with_tasks_completed_by_my_group: Optional[bool] = None, ) -> ProcessInstanceReportFilter: """Filter_from_metadata_with_overrides.""" report_filter = cls.filter_from_metadata(process_instance_report) @@ -190,5 +228,13 @@ class ProcessInstanceReportService: report_filter.end_to = end_to if process_status is not None: report_filter.process_status = process_status.split(",") + if initiated_by_me is not None: + report_filter.initiated_by_me = initiated_by_me + if with_tasks_completed_by_me is not None: + report_filter.with_tasks_completed_by_me = with_tasks_completed_by_me + if with_tasks_completed_by_my_group is not None: + report_filter.with_tasks_completed_by_my_group = ( + with_tasks_completed_by_my_group + ) return report_filter diff --git a/spiffworkflow-frontend/src/components/MyCompletedInstances.tsx b/spiffworkflow-frontend/src/components/MyCompletedInstances.tsx index fe6652951..2d0fe26a7 100644 --- a/spiffworkflow-frontend/src/components/MyCompletedInstances.tsx +++ b/spiffworkflow-frontend/src/components/MyCompletedInstances.tsx @@ -8,6 +8,8 @@ export default function MyCompletedInstances() { filtersEnabled={false} paginationQueryParamPrefix={paginationQueryParamPrefix} perPageOptions={[2, 5, 25]} + reportIdentifier="system_report_instances_initiated_by_me" + showReports={false} /> ); } diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 5b60f2178..20a2b2bb4 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -57,6 +57,7 @@ type OwnProps = { paginationQueryParamPrefix?: string; perPageOptions?: number[]; showReports?: boolean; + reportIdentifier?: string; }; interface dateParameters { @@ -69,6 +70,7 @@ export default function ProcessInstanceListTable({ paginationQueryParamPrefix, perPageOptions, showReports = true, + reportIdentifier, }: OwnProps) { const params = useParams(); const [searchParams] = useSearchParams(); @@ -173,9 +175,14 @@ export default function ProcessInstanceListTable({ queryParamString += `&user_filter=${userAppliedFilter}`; } - const reportIdentifier = searchParams.get('report_identifier'); - if (reportIdentifier) { - queryParamString += `&report_identifier=${reportIdentifier}`; + let reportIdentifierToUse: any = reportIdentifier; + + if (!reportIdentifierToUse) { + reportIdentifierToUse = searchParams.get('report_identifier'); + } + + if (reportIdentifierToUse) { + queryParamString += `&report_identifier=${reportIdentifierToUse}`; } Object.keys(dateParametersToAlwaysFilterBy).forEach( @@ -273,6 +280,7 @@ export default function ProcessInstanceListTable({ paginationQueryParamPrefix, processModelFullIdentifier, perPageOptions, + reportIdentifier, ]); // This sets the filter data using the saved reports returned from the initial instance_list query. diff --git a/spiffworkflow-frontend/src/routes/CompletedInstances.tsx b/spiffworkflow-frontend/src/routes/CompletedInstances.tsx index 237c21f3b..de5036776 100644 --- a/spiffworkflow-frontend/src/routes/CompletedInstances.tsx +++ b/spiffworkflow-frontend/src/routes/CompletedInstances.tsx @@ -1,5 +1,32 @@ -import MyCompletedInstances from '../components/MyCompletedInstances'; +import ProcessInstanceListTable from '../components/ProcessInstanceListTable'; export default function CompletedInstances() { - return ; + return ( + <> +

Initiated By Me

+ +

With Tasks Completed By Me

+ +

With Tasks Completed By My Group

+ + + ); }