From 3984aa16aa690b96f56b5068c45b46090047368b Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 25 Apr 2023 17:21:38 -0400 Subject: [PATCH 01/18] attempting to filter process instances with json instead of report filters w/ burnettk --- .../src/spiffworkflow_backend/api.yml | 4 +- .../models/process_instance_report.py | 16 +- .../routes/process_instances_controller.py | 156 ++++---- .../process_instance_report_service.py | 358 +++++++++--------- .../components/ProcessInstanceListTable.tsx | 14 +- 5 files changed, 289 insertions(+), 259 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index c37bd380..e87cc395 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -697,7 +697,7 @@ paths: description: Base64 encoded json of report filter by. schema: type: string - get: + post: operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_list_for_me summary: Returns a list of process instances that are associated with me. tags: @@ -828,7 +828,7 @@ paths: description: Base64 encoded json of report filter by. schema: type: string - get: + post: operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_list summary: Returns a list of process instances. tags: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py index ad2041cb..2c8a442d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py @@ -22,7 +22,21 @@ from spiffworkflow_backend.services.process_instance_processor import ( ) -ReportMetadata = dict[str, Any] +class FilterValue(TypedDict): + field_name: str + field_value: str + operator: str + + +class ReportMetadataColumn(TypedDict): + Header: str + accessor: str + + +class ReportMetadata(TypedDict): + columns: list[ReportMetadataColumn] + filter_by: list[FilterValue] + order_by: list[str] class ProcessInstanceReportAlreadyExistsError(Exception): 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 5b74ba03..dc0f2d76 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -226,103 +226,107 @@ def process_instance_resume( def process_instance_list_for_me( + body: Dict[str, Any], process_model_identifier: Optional[str] = None, page: int = 1, per_page: int = 100, - start_from: Optional[int] = None, - start_to: Optional[int] = None, - end_from: Optional[int] = None, - end_to: Optional[int] = None, - process_status: Optional[str] = None, - user_filter: Optional[bool] = False, - report_identifier: Optional[str] = None, - report_id: Optional[int] = None, - user_group_identifier: Optional[str] = None, - process_initiator_username: Optional[str] = None, - report_columns: Optional[str] = None, - report_filter_by: Optional[str] = None, + # start_from: Optional[int] = None, + # start_to: Optional[int] = None, + # end_from: Optional[int] = None, + # end_to: Optional[int] = None, + # process_status: Optional[str] = None, + # user_filter: Optional[bool] = False, + # report_identifier: Optional[str] = None, + # report_id: Optional[int] = None, + # user_group_identifier: Optional[str] = None, + # process_initiator_username: Optional[str] = None, + # report_columns: Optional[str] = None, + # report_filter_by: Optional[str] = None, ) -> flask.wrappers.Response: """Process_instance_list_for_me.""" return process_instance_list( process_model_identifier=process_model_identifier, page=page, per_page=per_page, - start_from=start_from, - start_to=start_to, - end_from=end_from, - end_to=end_to, - process_status=process_status, - user_filter=user_filter, - report_identifier=report_identifier, - report_id=report_id, - user_group_identifier=user_group_identifier, - with_relation_to_me=True, - report_columns=report_columns, - report_filter_by=report_filter_by, - process_initiator_username=process_initiator_username, + # start_from=start_from, + # start_to=start_to, + # end_from=end_from, + # end_to=end_to, + # process_status=process_status, + # user_filter=user_filter, + # report_identifier=report_identifier, + # report_id=report_id, + # user_group_identifier=user_group_identifier, + # with_relation_to_me=True, + # report_columns=report_columns, + # report_filter_by=report_filter_by, + # process_initiator_username=process_initiator_username, + body=body ) def process_instance_list( + body: Dict[str, Any], process_model_identifier: Optional[str] = None, page: int = 1, per_page: int = 100, - start_from: Optional[int] = None, - start_to: Optional[int] = None, - end_from: Optional[int] = None, - end_to: Optional[int] = None, - process_status: Optional[str] = None, - with_relation_to_me: Optional[bool] = None, - user_filter: Optional[bool] = False, - report_identifier: Optional[str] = None, - report_id: Optional[int] = None, - user_group_identifier: Optional[str] = None, - process_initiator_username: Optional[str] = None, - report_columns: Optional[str] = None, - report_filter_by: Optional[str] = None, + # start_from: Optional[int] = None, + # start_to: Optional[int] = None, + # end_from: Optional[int] = None, + # end_to: Optional[int] = None, + # process_status: Optional[str] = None, + # with_relation_to_me: Optional[bool] = None, + # user_filter: Optional[bool] = False, + # report_identifier: Optional[str] = None, + # report_id: Optional[int] = None, + # user_group_identifier: Optional[str] = None, + # process_initiator_username: Optional[str] = None, + # report_columns: Optional[str] = None, + # report_filter_by: Optional[str] = None, ) -> flask.wrappers.Response: - """Process_instance_list.""" - process_instance_report = ProcessInstanceReportService.report_with_identifier(g.user, report_id, report_identifier) + # process_instance_report = ProcessInstanceReportService.report_with_identifier(g.user, body['report_id'], body['report_identifier']) + process_instance_report = ProcessInstanceReportService.report_with_identifier(g.user) - report_column_list = None - if report_columns: - report_column_list = json.loads(base64.b64decode(report_columns)) - report_filter_by_list = None - if report_filter_by: - report_filter_by_list = json.loads(base64.b64decode(report_filter_by)) + # report_column_list = None + # if report_columns: + # report_column_list = json.loads(base64.b64decode(report_columns)) + # report_filter_by_list = None + # if report_filter_by: + # report_filter_by_list = json.loads(base64.b64decode(report_filter_by)) - if user_filter: - report_filter = ProcessInstanceReportFilter( - 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, - with_relation_to_me=with_relation_to_me, - process_status=process_status.split(",") if process_status else None, - process_initiator_username=process_initiator_username, - report_column_list=report_column_list, - report_filter_by_list=report_filter_by_list, - ) - else: - report_filter = ProcessInstanceReportService.filter_from_metadata_with_overrides( - process_instance_report=process_instance_report, - 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, - with_relation_to_me=with_relation_to_me, - process_initiator_username=process_initiator_username, - report_column_list=report_column_list, - report_filter_by_list=report_filter_by_list, - ) + # if user_filter: + # report_filter = ProcessInstanceReportFilter( + # 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, + # # with_relation_to_me=with_relation_to_me, + # # process_status=process_status.split(",") if process_status else None, + # # process_initiator_username=process_initiator_username, + # # report_column_list=report_column_list, + # # report_filter_by_list=report_filter_by_list, + # ) + # else: + # report_filter = ProcessInstanceReportService.filter_from_metadata_with_overrides( + # process_instance_report=process_instance_report, + # 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, + # # with_relation_to_me=with_relation_to_me, + # # process_initiator_username=process_initiator_username, + # # report_column_list=report_column_list, + # # report_filter_by_list=report_filter_by_list, + # ) response_json = ProcessInstanceReportService.run_process_instance_report( - report_filter=report_filter, + # report_filter=report_filter, + report_metadata=body['report_metadata'], process_instance_report=process_instance_report, page=page, per_page=per_page, 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 04510969..a4cadb96 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 @@ -1,7 +1,7 @@ """Process_instance_report_service.""" import re from dataclasses import dataclass -from typing import Any +from typing import Any, Generator, Iterable from typing import Optional from typing import Type @@ -24,7 +24,9 @@ from spiffworkflow_backend.models.process_instance_metadata import ( ProcessInstanceMetadataModel, ) from spiffworkflow_backend.models.process_instance_report import ( + FilterValue, ProcessInstanceReportModel, + ReportMetadata, ) from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel @@ -236,7 +238,6 @@ class ProcessInstanceReportService: report_id: Optional[int] = None, report_identifier: Optional[str] = None, ) -> ProcessInstanceReportModel: - """Report_with_filter.""" if report_id is not None: process_instance_report = ProcessInstanceReportModel.query.filter_by( id=report_id, created_by_id=user.id @@ -496,25 +497,31 @@ class ProcessInstanceReportService: {"Header": "Status", "accessor": "status", "filterable": False}, ] + @classmethod + def blah(cls, filters: list[FilterValue], filter_key: str) -> Generator: + for filter in filters: + if filter['field_name'] == filter_key and filter['field_value'] is not None: + yield filter['field_value'] + @classmethod def run_process_instance_report( cls, - report_filter: ProcessInstanceReportFilter, + # report_filter: ProcessInstanceReportFilter, + report_metadata: ReportMetadata, process_instance_report: ProcessInstanceReportModel, user: UserModel, page: int = 1, per_page: int = 100, ) -> dict: - """Run_process_instance_report.""" process_instance_query = ProcessInstanceModel.query # Always join that hot user table for good performance at serialization time. process_instance_query = process_instance_query.options(selectinload(ProcessInstanceModel.process_initiator)) + filters = report_metadata['filter_by'] - if report_filter.process_model_identifier is not None: + for value in cls.blah(filters, 'process_model_identifier'): process_model = ProcessModelService.get_process_model( - f"{report_filter.process_model_identifier}", + f"{value}", ) - process_instance_query = process_instance_query.filter_by(process_model_identifier=process_model.id) # this can never happen. obviously the class has the columns it defines. this is just to appease mypy. @@ -527,187 +534,188 @@ class ProcessInstanceReportService: ) ) - if report_filter.start_from is not None: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.start_in_seconds >= report_filter.start_from - ) - if report_filter.start_to is not None: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.start_in_seconds <= report_filter.start_to - ) - if report_filter.end_from is not None: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.end_in_seconds >= report_filter.end_from - ) - if report_filter.end_to is not None: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.end_in_seconds <= report_filter.end_to - ) - if report_filter.process_status is not None: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(report_filter.process_status) # type: ignore - ) - - if report_filter.initiated_by_me is True: - process_instance_query = process_instance_query.filter_by(process_initiator=user) - - if report_filter.has_terminal_status is True: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore - ) - elif report_filter.has_terminal_status is False: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.not_in(ProcessInstanceModel.terminal_statuses()) # type: ignore - ) - if report_filter.has_active_status is True: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(ProcessInstanceModel.active_statuses()) # type: ignore - ) - - if report_filter.process_initiator_username is not None: - initiator = UserModel.query.filter_by(username=report_filter.process_initiator_username).first() - process_initiator_id = -1 - if initiator: - process_initiator_id = initiator.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 - and not report_filter.with_tasks_i_can_complete - and report_filter.with_relation_to_me is True - ): - process_instance_query = process_instance_query.outerjoin(HumanTaskModel).outerjoin( - HumanTaskUserModel, - and_( - HumanTaskModel.id == HumanTaskUserModel.human_task_id, - HumanTaskUserModel.user_id == user.id, - ), - ) - process_instance_query = process_instance_query.filter( - or_( - HumanTaskUserModel.id.is_not(None), - ProcessInstanceModel.process_initiator_id == user.id, - ) - ) - - if report_filter.with_tasks_completed_by_me is True: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.process_initiator_id != user.id - ) - process_instance_query = process_instance_query.join( - HumanTaskModel, - and_( - HumanTaskModel.process_instance_id == ProcessInstanceModel.id, - HumanTaskModel.completed_by_user_id == user.id, - ), - ) - - if report_filter.with_tasks_i_can_complete is True: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.process_initiator_id != user.id - ) - process_instance_query = process_instance_query.join( - HumanTaskModel, - and_( - HumanTaskModel.process_instance_id == ProcessInstanceModel.id, - HumanTaskModel.lane_assignment_id.is_(None), # type: ignore - ), - ).join( - HumanTaskUserModel, - and_(HumanTaskUserModel.human_task_id == HumanTaskModel.id, HumanTaskUserModel.user_id == user.id), - ) - if report_filter.has_active_status: - process_instance_query = process_instance_query.filter( - HumanTaskModel.completed.is_(False) # type: ignore - ) - - if report_filter.with_tasks_assigned_to_my_group is True: - group_model_join_conditions = [GroupModel.id == HumanTaskModel.lane_assignment_id] - if report_filter.user_group_identifier: - group_model_join_conditions.append(GroupModel.identifier == report_filter.user_group_identifier) - - process_instance_query = process_instance_query.join(HumanTaskModel) - if report_filter.has_active_status: - process_instance_query = process_instance_query.filter( - HumanTaskModel.completed.is_(False) # type: ignore - ) - - process_instance_query = process_instance_query.join(GroupModel, and_(*group_model_join_conditions)) - process_instance_query = process_instance_query.join( - UserGroupAssignmentModel, - UserGroupAssignmentModel.group_id == GroupModel.id, - ) - process_instance_query = process_instance_query.filter(UserGroupAssignmentModel.user_id == user.id) - - instance_metadata_aliases = {} - stock_columns = cls.get_column_names_for_model(ProcessInstanceModel) - if isinstance(report_filter.report_column_list, list): - process_instance_report.report_metadata["columns"] = report_filter.report_column_list - if isinstance(report_filter.report_filter_by_list, list): - process_instance_report.report_metadata["filter_by"] = report_filter.report_filter_by_list - - for column in process_instance_report.report_metadata["columns"]: - if column["accessor"] in stock_columns: - continue - instance_metadata_alias = aliased(ProcessInstanceMetadataModel) - instance_metadata_aliases[column["accessor"]] = instance_metadata_alias - - filter_for_column = None - if "filter_by" in process_instance_report.report_metadata: - filter_for_column = next( - ( - f - for f in process_instance_report.report_metadata["filter_by"] - if f["field_name"] == column["accessor"] - ), - None, - ) - isouter = True - conditions = [ - ProcessInstanceModel.id == instance_metadata_alias.process_instance_id, - instance_metadata_alias.key == column["accessor"], - ] - if filter_for_column: - isouter = False - conditions.append(instance_metadata_alias.value == filter_for_column["field_value"]) - process_instance_query = process_instance_query.join( - instance_metadata_alias, and_(*conditions), isouter=isouter - ).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"])) - - order_by_query_array = [] - order_by_array = process_instance_report.report_metadata["order_by"] - if len(order_by_array) < 1: - order_by_array = ProcessInstanceReportModel.default_order_by() - for order_by_option in order_by_array: - attribute = re.sub("^-", "", order_by_option) - if attribute in stock_columns: - if order_by_option.startswith("-"): - order_by_query_array.append(getattr(ProcessInstanceModel, attribute).desc()) - else: - order_by_query_array.append(getattr(ProcessInstanceModel, attribute).asc()) - elif attribute in instance_metadata_aliases: - if order_by_option.startswith("-"): - order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).desc()) - else: - order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).asc()) + # if report_filter.start_from is not None: + # process_instance_query = process_instance_query.filter( + # ProcessInstanceModel.start_in_seconds >= report_filter.start_from + # ) + # if report_filter.start_to is not None: + # process_instance_query = process_instance_query.filter( + # ProcessInstanceModel.start_in_seconds <= report_filter.start_to + # ) + # if report_filter.end_from is not None: + # process_instance_query = process_instance_query.filter( + # ProcessInstanceModel.end_in_seconds >= report_filter.end_from + # ) + # if report_filter.end_to is not None: + # process_instance_query = process_instance_query.filter( + # ProcessInstanceModel.end_in_seconds <= report_filter.end_to + # ) + # if report_filter.process_status is not None: + # process_instance_query = process_instance_query.filter( + # ProcessInstanceModel.status.in_(report_filter.process_status) # type: ignore + # ) + # + # if report_filter.initiated_by_me is True: + # process_instance_query = process_instance_query.filter_by(process_initiator=user) + # + # if report_filter.has_terminal_status is True: + # process_instance_query = process_instance_query.filter( + # ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore + # ) + # elif report_filter.has_terminal_status is False: + # process_instance_query = process_instance_query.filter( + # ProcessInstanceModel.status.not_in(ProcessInstanceModel.terminal_statuses()) # type: ignore + # ) + # if report_filter.has_active_status is True: + # process_instance_query = process_instance_query.filter( + # ProcessInstanceModel.status.in_(ProcessInstanceModel.active_statuses()) # type: ignore + # ) + # + # if report_filter.process_initiator_username is not None: + # initiator = UserModel.query.filter_by(username=report_filter.process_initiator_username).first() + # process_initiator_id = -1 + # if initiator: + # process_initiator_id = initiator.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 + # and not report_filter.with_tasks_i_can_complete + # and report_filter.with_relation_to_me is True + # ): + # process_instance_query = process_instance_query.outerjoin(HumanTaskModel).outerjoin( + # HumanTaskUserModel, + # and_( + # HumanTaskModel.id == HumanTaskUserModel.human_task_id, + # HumanTaskUserModel.user_id == user.id, + # ), + # ) + # process_instance_query = process_instance_query.filter( + # or_( + # HumanTaskUserModel.id.is_not(None), + # ProcessInstanceModel.process_initiator_id == user.id, + # ) + # ) + # + # if report_filter.with_tasks_completed_by_me is True: + # process_instance_query = process_instance_query.filter( + # ProcessInstanceModel.process_initiator_id != user.id + # ) + # process_instance_query = process_instance_query.join( + # HumanTaskModel, + # and_( + # HumanTaskModel.process_instance_id == ProcessInstanceModel.id, + # HumanTaskModel.completed_by_user_id == user.id, + # ), + # ) + # + # if report_filter.with_tasks_i_can_complete is True: + # process_instance_query = process_instance_query.filter( + # ProcessInstanceModel.process_initiator_id != user.id + # ) + # process_instance_query = process_instance_query.join( + # HumanTaskModel, + # and_( + # HumanTaskModel.process_instance_id == ProcessInstanceModel.id, + # HumanTaskModel.lane_assignment_id.is_(None), # type: ignore + # ), + # ).join( + # HumanTaskUserModel, + # and_(HumanTaskUserModel.human_task_id == HumanTaskModel.id, HumanTaskUserModel.user_id == user.id), + # ) + # if report_filter.has_active_status: + # process_instance_query = process_instance_query.filter( + # HumanTaskModel.completed.is_(False) # type: ignore + # ) + # + # if report_filter.with_tasks_assigned_to_my_group is True: + # group_model_join_conditions = [GroupModel.id == HumanTaskModel.lane_assignment_id] + # if report_filter.user_group_identifier: + # group_model_join_conditions.append(GroupModel.identifier == report_filter.user_group_identifier) + # + # process_instance_query = process_instance_query.join(HumanTaskModel) + # if report_filter.has_active_status: + # process_instance_query = process_instance_query.filter( + # HumanTaskModel.completed.is_(False) # type: ignore + # ) + # + # process_instance_query = process_instance_query.join(GroupModel, and_(*group_model_join_conditions)) + # process_instance_query = process_instance_query.join( + # UserGroupAssignmentModel, + # UserGroupAssignmentModel.group_id == GroupModel.id, + # ) + # process_instance_query = process_instance_query.filter(UserGroupAssignmentModel.user_id == user.id) + # + # instance_metadata_aliases = {} + # stock_columns = cls.get_column_names_for_model(ProcessInstanceModel) + # if isinstance(report_filter.report_column_list, list): + # process_instance_report.report_metadata["columns"] = report_filter.report_column_list + # if isinstance(report_filter.report_filter_by_list, list): + # process_instance_report.report_metadata["filter_by"] = report_filter.report_filter_by_list + # + # for column in process_instance_report.report_metadata["columns"]: + # if column["accessor"] in stock_columns: + # continue + # instance_metadata_alias = aliased(ProcessInstanceMetadataModel) + # instance_metadata_aliases[column["accessor"]] = instance_metadata_alias + # + # filter_for_column = None + # if "filter_by" in process_instance_report.report_metadata: + # filter_for_column = next( + # ( + # f + # for f in process_instance_report.report_metadata["filter_by"] + # if f["field_name"] == column["accessor"] + # ), + # None, + # ) + # isouter = True + # conditions = [ + # ProcessInstanceModel.id == instance_metadata_alias.process_instance_id, + # instance_metadata_alias.key == column["accessor"], + # ] + # if filter_for_column: + # isouter = False + # conditions.append(instance_metadata_alias.value == filter_for_column["field_value"]) + # process_instance_query = process_instance_query.join( + # instance_metadata_alias, and_(*conditions), isouter=isouter + # ).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"])) + # + # order_by_query_array = [] + # order_by_array = process_instance_report.report_metadata["order_by"] + # if len(order_by_array) < 1: + # order_by_array = ProcessInstanceReportModel.default_order_by() + # for order_by_option in order_by_array: + # attribute = re.sub("^-", "", order_by_option) + # if attribute in stock_columns: + # if order_by_option.startswith("-"): + # order_by_query_array.append(getattr(ProcessInstanceModel, attribute).desc()) + # else: + # order_by_query_array.append(getattr(ProcessInstanceModel, attribute).asc()) + # elif attribute in instance_metadata_aliases: + # if order_by_option.startswith("-"): + # order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).desc()) + # else: + # order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).asc()) process_instances = ( process_instance_query.group_by(ProcessInstanceModel.id) .add_columns(ProcessInstanceModel.id) - .order_by(*order_by_query_array) + # .order_by(*order_by_query_array) .paginate(page=page, per_page=per_page, error_out=False) ) results = cls.add_metadata_columns_to_process_instance( process_instances.items, process_instance_report.report_metadata["columns"] ) - if report_filter.oldest_open_human_task_fields: - results = cls.add_human_task_fields(results, report_filter.oldest_open_human_task_fields) + # if report_filter.oldest_open_human_task_fields: + # results = cls.add_human_task_fields(results, report_filter.oldest_open_human_task_fields) response_json = { "report": process_instance_report, "results": results, - "filters": report_filter.to_dict(), + "filters": filters, + "hash": "HEY", "pagination": { "count": len(results), "total": process_instances.total, diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index ccd62075..112957f6 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -114,12 +114,12 @@ export default function ProcessInstanceListTable({ canCompleteAllTasks = false, showActionsColumn = false, }: OwnProps) { - let apiPath = '/process-instances/for-me'; + let processInstanceApiSearchPath = '/process-instances/for-me'; if (variant === 'all') { - apiPath = '/process-instances'; + processInstanceApiSearchPath = '/process-instances'; } const params = useParams(); - const [searchParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const { addError, removeError } = useAPIError(); @@ -284,6 +284,8 @@ export default function ProcessInstanceListTable({ if (result.report.id) { setProcessInstanceReportSelection(result.report); } + // searchParams.set('key', result.hash); + // setSearchParams(searchParams); } const stopRefreshing = () => { if (clearRefreshRef.current) { @@ -372,9 +374,11 @@ export default function ProcessInstanceListTable({ } HttpService.makeCallToBackend({ - path: `${apiPath}?${queryParamString}`, + path: `${processInstanceApiSearchPath}?${queryParamString}`, successCallback: setProcessInstancesFromResult, + httpMethod: 'POST', onUnauthorized: stopRefreshing, + postBody: {'report_metadata': {'filter_by': [{'field_name': 'process_model_identifier', 'field_value': 'example/with-milestones' }]}}, }); } function processResultForProcessModels(result: any) { @@ -441,7 +445,7 @@ export default function ProcessInstanceListTable({ perPageOptions, reportIdentifier, additionalParams, - apiPath, + processInstanceApiSearchPath, ]); // This sets the filter data using the saved reports returned from the initial instance_list query. From 8ac5f0228f37e8180bab4af69257b46c3c06184e Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 26 Apr 2023 11:06:01 -0400 Subject: [PATCH 02/18] can somewhat search now w/ burnettk --- .../routes/process_instances_controller.py | 6 +- .../process_instance_report_service.py | 757 +++++++++--------- .../components/ProcessInstanceListTable.tsx | 120 ++- spiffworkflow-frontend/src/interfaces.ts | 2 +- 4 files changed, 494 insertions(+), 391 deletions(-) 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 dc0f2d76..65821bfb 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -68,9 +68,9 @@ from spiffworkflow_backend.services.process_instance_queue_service import ( from spiffworkflow_backend.services.process_instance_queue_service import ( ProcessInstanceQueueService, ) -from spiffworkflow_backend.services.process_instance_report_service import ( - ProcessInstanceReportFilter, -) +# from spiffworkflow_backend.services.process_instance_report_service import ( +# ProcessInstanceReportFilter, +# ) from spiffworkflow_backend.services.process_instance_report_service import ( ProcessInstanceReportService, ) 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 a4cadb96..92040b2b 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 @@ -37,71 +37,71 @@ class ProcessInstanceReportNotFoundError(Exception): """ProcessInstanceReportNotFoundError.""" -@dataclass -class ProcessInstanceReportFilter: - """ProcessInstanceReportFilter.""" - - process_model_identifier: Optional[str] = None - user_group_identifier: Optional[str] = None - start_from: Optional[int] = None - start_to: Optional[int] = None - end_from: Optional[int] = None - end_to: Optional[int] = None - process_status: Optional[list[str]] = None - initiated_by_me: Optional[bool] = None - has_terminal_status: Optional[bool] = None - has_active_status: Optional[bool] = None - with_tasks_completed_by_me: Optional[bool] = None - with_tasks_i_can_complete: 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 - report_column_list: Optional[list] = None - report_filter_by_list: Optional[list] = None - oldest_open_human_task_fields: Optional[list] = None - - def to_dict(self) -> dict[str, str]: - """To_dict.""" - d = {} - - if self.process_model_identifier is not None: - d["process_model_identifier"] = self.process_model_identifier - if self.user_group_identifier is not None: - d["user_group_identifier"] = self.user_group_identifier - if self.start_from is not None: - d["start_from"] = str(self.start_from) - if self.start_to is not None: - d["start_to"] = str(self.start_to) - if self.end_from is not None: - d["end_from"] = str(self.end_from) - if self.end_to is not None: - 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.has_terminal_status is not None: - d["has_terminal_status"] = str(self.has_terminal_status).lower() - if self.has_active_status is not None: - d["has_active_status"] = str(self.has_active_status).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_i_can_complete is not None: - d["with_tasks_i_can_complete"] = str(self.with_tasks_i_can_complete).lower() - if self.with_tasks_assigned_to_my_group is not None: - d["with_tasks_assigned_to_my_group"] = str(self.with_tasks_assigned_to_my_group).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) - if self.report_column_list is not None: - d["report_column_list"] = str(self.report_column_list) - if self.report_filter_by_list is not None: - d["report_filter_by_list"] = str(self.report_filter_by_list) - if self.oldest_open_human_task_fields is not None: - d["oldest_open_human_task_fields"] = str(self.oldest_open_human_task_fields) - - return d +# @dataclass +# class ProcessInstanceReportFilter: +# """ProcessInstanceReportFilter.""" +# +# process_model_identifier: Optional[str] = None +# user_group_identifier: Optional[str] = None +# start_from: Optional[int] = None +# start_to: Optional[int] = None +# end_from: Optional[int] = None +# end_to: Optional[int] = None +# process_status: Optional[list[str]] = None +# initiated_by_me: Optional[bool] = None +# has_terminal_status: Optional[bool] = None +# has_active_status: Optional[bool] = None +# with_tasks_completed_by_me: Optional[bool] = None +# with_tasks_i_can_complete: 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 +# report_column_list: Optional[list] = None +# report_filter_by_list: Optional[list] = None +# oldest_open_human_task_fields: Optional[list] = None +# +# def to_dict(self) -> dict[str, str]: +# """To_dict.""" +# d = {} +# +# if self.process_model_identifier is not None: +# d["process_model_identifier"] = self.process_model_identifier +# if self.user_group_identifier is not None: +# d["user_group_identifier"] = self.user_group_identifier +# if self.start_from is not None: +# d["start_from"] = str(self.start_from) +# if self.start_to is not None: +# d["start_to"] = str(self.start_to) +# if self.end_from is not None: +# d["end_from"] = str(self.end_from) +# if self.end_to is not None: +# 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.has_terminal_status is not None: +# d["has_terminal_status"] = str(self.has_terminal_status).lower() +# if self.has_active_status is not None: +# d["has_active_status"] = str(self.has_active_status).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_i_can_complete is not None: +# d["with_tasks_i_can_complete"] = str(self.with_tasks_i_can_complete).lower() +# if self.with_tasks_assigned_to_my_group is not None: +# d["with_tasks_assigned_to_my_group"] = str(self.with_tasks_assigned_to_my_group).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) +# if self.report_column_list is not None: +# d["report_column_list"] = str(self.report_column_list) +# if self.report_filter_by_list is not None: +# d["report_filter_by_list"] = str(self.report_filter_by_list) +# if self.oldest_open_human_task_fields is not None: +# d["oldest_open_human_task_fields"] = str(self.oldest_open_human_task_fields) +# +# return d class ProcessInstanceReportService: @@ -268,141 +268,141 @@ class ProcessInstanceReportService: return process_instance_report # type: ignore - @classmethod - def filter_by_to_dict(cls, process_instance_report: ProcessInstanceReportModel) -> dict[str, str]: - """Filter_by_to_dict.""" - metadata = process_instance_report.report_metadata - filter_by = metadata.get("filter_by", []) - filters = {d["field_name"]: d["field_value"] for d in filter_by if "field_name" in d and "field_value" in d} - return filters - - @classmethod - def filter_from_metadata(cls, process_instance_report: ProcessInstanceReportModel) -> ProcessInstanceReportFilter: - """Filter_from_metadata.""" - filters = cls.filter_by_to_dict(process_instance_report) - - def bool_value(key: str) -> Optional[bool]: - """Bool_value.""" - if key not in filters: - return None - # bool returns True if not an empty string so check explicitly for false - if filters[key] in ["false", "False"]: - return False - return bool(filters[key]) - - def int_value(key: str) -> Optional[int]: - """Int_value.""" - return int(filters[key]) if key in filters else None - - def list_value(key: str) -> Optional[list[str]]: - return filters[key].split(",") if key in filters else None - - process_model_identifier = filters.get("process_model_identifier") - user_group_identifier = filters.get("user_group_identifier") - start_from = int_value("start_from") - start_to = int_value("start_to") - 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") - has_terminal_status = bool_value("has_terminal_status") - has_active_status = bool_value("has_active_status") - with_tasks_completed_by_me = bool_value("with_tasks_completed_by_me") - with_tasks_i_can_complete = bool_value("with_tasks_i_can_complete") - 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_column_list = list_value("report_column_list") - report_filter_by_list = list_value("report_filter_by_list") - oldest_open_human_task_fields = list_value("oldest_open_human_task_fields") - - report_filter = ProcessInstanceReportFilter( - 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, - has_active_status=has_active_status, - with_tasks_completed_by_me=with_tasks_completed_by_me, - with_tasks_i_can_complete=with_tasks_i_can_complete, - 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, - report_column_list=report_column_list, - report_filter_by_list=report_filter_by_list, - oldest_open_human_task_fields=oldest_open_human_task_fields, - ) - - return report_filter - - @classmethod - def filter_from_metadata_with_overrides( - cls, - process_instance_report: ProcessInstanceReportModel, - process_model_identifier: Optional[str] = None, - user_group_identifier: Optional[str] = None, - start_from: Optional[int] = None, - start_to: Optional[int] = None, - end_from: Optional[int] = None, - end_to: Optional[int] = None, - process_status: Optional[str] = None, - initiated_by_me: Optional[bool] = None, - has_terminal_status: Optional[bool] = None, - has_active_status: Optional[bool] = None, - with_tasks_completed_by_me: Optional[bool] = None, - with_tasks_i_can_complete: 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, - report_column_list: Optional[list] = None, - report_filter_by_list: Optional[list] = None, - oldest_open_human_task_fields: Optional[list] = None, - ) -> ProcessInstanceReportFilter: - """Filter_from_metadata_with_overrides.""" - report_filter = cls.filter_from_metadata(process_instance_report) - - if process_model_identifier is not None: - report_filter.process_model_identifier = process_model_identifier - if user_group_identifier is not None: - report_filter.user_group_identifier = user_group_identifier - if start_from is not None: - report_filter.start_from = start_from - if start_to is not None: - report_filter.start_to = start_to - if end_from is not None: - report_filter.end_from = end_from - if end_to is not None: - 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 has_terminal_status is not None: - report_filter.has_terminal_status = has_terminal_status - if has_active_status is not None: - report_filter.has_active_status = has_active_status - if with_tasks_completed_by_me is not None: - report_filter.with_tasks_completed_by_me = with_tasks_completed_by_me - if with_tasks_i_can_complete is not None: - report_filter.with_tasks_i_can_complete = with_tasks_i_can_complete - if process_initiator_username is not None: - report_filter.process_initiator_username = process_initiator_username - if report_column_list is not None: - report_filter.report_column_list = report_column_list - if report_filter_by_list is not None: - report_filter.report_filter_by_list = report_filter_by_list - if oldest_open_human_task_fields is not None: - report_filter.oldest_open_human_task_fields = oldest_open_human_task_fields - if with_tasks_assigned_to_my_group is not None: - report_filter.with_tasks_assigned_to_my_group = with_tasks_assigned_to_my_group - if with_relation_to_me is not None: - report_filter.with_relation_to_me = with_relation_to_me - - return report_filter + # @classmethod + # def filter_by_to_dict(cls, process_instance_report: ProcessInstanceReportModel) -> dict[str, str]: + # """Filter_by_to_dict.""" + # metadata = process_instance_report.report_metadata + # filter_by = metadata.get("filter_by", []) + # filters = {d["field_name"]: d["field_value"] for d in filter_by if "field_name" in d and "field_value" in d} + # return filters + # + # @classmethod + # def filter_from_metadata(cls, process_instance_report: ProcessInstanceReportModel) -> ProcessInstanceReportFilter: + # """Filter_from_metadata.""" + # filters = cls.filter_by_to_dict(process_instance_report) + # + # def bool_value(key: str) -> Optional[bool]: + # """Bool_value.""" + # if key not in filters: + # return None + # # bool returns True if not an empty string so check explicitly for false + # if filters[key] in ["false", "False"]: + # return False + # return bool(filters[key]) + # + # def int_value(key: str) -> Optional[int]: + # """Int_value.""" + # return int(filters[key]) if key in filters else None + # + # def list_value(key: str) -> Optional[list[str]]: + # return filters[key].split(",") if key in filters else None + # + # process_model_identifier = filters.get("process_model_identifier") + # user_group_identifier = filters.get("user_group_identifier") + # start_from = int_value("start_from") + # start_to = int_value("start_to") + # 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") + # has_terminal_status = bool_value("has_terminal_status") + # has_active_status = bool_value("has_active_status") + # with_tasks_completed_by_me = bool_value("with_tasks_completed_by_me") + # with_tasks_i_can_complete = bool_value("with_tasks_i_can_complete") + # 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_column_list = list_value("report_column_list") + # report_filter_by_list = list_value("report_filter_by_list") + # oldest_open_human_task_fields = list_value("oldest_open_human_task_fields") + # + # report_filter = ProcessInstanceReportFilter( + # 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, + # has_active_status=has_active_status, + # with_tasks_completed_by_me=with_tasks_completed_by_me, + # with_tasks_i_can_complete=with_tasks_i_can_complete, + # 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, + # report_column_list=report_column_list, + # report_filter_by_list=report_filter_by_list, + # oldest_open_human_task_fields=oldest_open_human_task_fields, + # ) + # + # return report_filter + # + # @classmethod + # def filter_from_metadata_with_overrides( + # cls, + # process_instance_report: ProcessInstanceReportModel, + # process_model_identifier: Optional[str] = None, + # user_group_identifier: Optional[str] = None, + # start_from: Optional[int] = None, + # start_to: Optional[int] = None, + # end_from: Optional[int] = None, + # end_to: Optional[int] = None, + # process_status: Optional[str] = None, + # initiated_by_me: Optional[bool] = None, + # has_terminal_status: Optional[bool] = None, + # has_active_status: Optional[bool] = None, + # with_tasks_completed_by_me: Optional[bool] = None, + # with_tasks_i_can_complete: 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, + # report_column_list: Optional[list] = None, + # report_filter_by_list: Optional[list] = None, + # oldest_open_human_task_fields: Optional[list] = None, + # ) -> ProcessInstanceReportFilter: + # """Filter_from_metadata_with_overrides.""" + # report_filter = cls.filter_from_metadata(process_instance_report) + # + # if process_model_identifier is not None: + # report_filter.process_model_identifier = process_model_identifier + # if user_group_identifier is not None: + # report_filter.user_group_identifier = user_group_identifier + # if start_from is not None: + # report_filter.start_from = start_from + # if start_to is not None: + # report_filter.start_to = start_to + # if end_from is not None: + # report_filter.end_from = end_from + # if end_to is not None: + # 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 has_terminal_status is not None: + # report_filter.has_terminal_status = has_terminal_status + # if has_active_status is not None: + # report_filter.has_active_status = has_active_status + # if with_tasks_completed_by_me is not None: + # report_filter.with_tasks_completed_by_me = with_tasks_completed_by_me + # if with_tasks_i_can_complete is not None: + # report_filter.with_tasks_i_can_complete = with_tasks_i_can_complete + # if process_initiator_username is not None: + # report_filter.process_initiator_username = process_initiator_username + # if report_column_list is not None: + # report_filter.report_column_list = report_column_list + # if report_filter_by_list is not None: + # report_filter.report_filter_by_list = report_filter_by_list + # if oldest_open_human_task_fields is not None: + # report_filter.oldest_open_human_task_fields = oldest_open_human_task_fields + # if with_tasks_assigned_to_my_group is not None: + # report_filter.with_tasks_assigned_to_my_group = with_tasks_assigned_to_my_group + # if with_relation_to_me is not None: + # report_filter.with_relation_to_me = with_relation_to_me + # + # return report_filter @classmethod def add_metadata_columns_to_process_instance( @@ -498,10 +498,16 @@ class ProcessInstanceReportService: ] @classmethod - def blah(cls, filters: list[FilterValue], filter_key: str) -> Generator: + def get_filter_value(cls, filters: list[FilterValue], filter_key: str) -> Any: for filter in filters: if filter['field_name'] == filter_key and filter['field_value'] is not None: - yield filter['field_value'] + return filter['field_value'] + + @classmethod + def check_filter_value(cls, filters: list[FilterValue], filter_key: str) -> Generator: + value = cls.get_filter_value(filters, filter_key) + if value is not None: + yield value @classmethod def run_process_instance_report( @@ -518,7 +524,7 @@ class ProcessInstanceReportService: process_instance_query = process_instance_query.options(selectinload(ProcessInstanceModel.process_initiator)) filters = report_metadata['filter_by'] - for value in cls.blah(filters, 'process_model_identifier'): + for value in cls.check_filter_value(filters, 'process_model_identifier'): process_model = ProcessModelService.get_process_model( f"{value}", ) @@ -534,183 +540,194 @@ class ProcessInstanceReportService: ) ) - # if report_filter.start_from is not None: - # process_instance_query = process_instance_query.filter( - # ProcessInstanceModel.start_in_seconds >= report_filter.start_from - # ) - # if report_filter.start_to is not None: - # process_instance_query = process_instance_query.filter( - # ProcessInstanceModel.start_in_seconds <= report_filter.start_to - # ) - # if report_filter.end_from is not None: - # process_instance_query = process_instance_query.filter( - # ProcessInstanceModel.end_in_seconds >= report_filter.end_from - # ) - # if report_filter.end_to is not None: - # process_instance_query = process_instance_query.filter( - # ProcessInstanceModel.end_in_seconds <= report_filter.end_to - # ) - # if report_filter.process_status is not None: - # process_instance_query = process_instance_query.filter( - # ProcessInstanceModel.status.in_(report_filter.process_status) # type: ignore - # ) - # - # if report_filter.initiated_by_me is True: - # process_instance_query = process_instance_query.filter_by(process_initiator=user) - # - # if report_filter.has_terminal_status is True: - # process_instance_query = process_instance_query.filter( - # ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore - # ) - # elif report_filter.has_terminal_status is False: - # process_instance_query = process_instance_query.filter( - # ProcessInstanceModel.status.not_in(ProcessInstanceModel.terminal_statuses()) # type: ignore - # ) - # if report_filter.has_active_status is True: - # process_instance_query = process_instance_query.filter( - # ProcessInstanceModel.status.in_(ProcessInstanceModel.active_statuses()) # type: ignore - # ) - # - # if report_filter.process_initiator_username is not None: - # initiator = UserModel.query.filter_by(username=report_filter.process_initiator_username).first() - # process_initiator_id = -1 - # if initiator: - # process_initiator_id = initiator.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 - # and not report_filter.with_tasks_i_can_complete - # and report_filter.with_relation_to_me is True - # ): - # process_instance_query = process_instance_query.outerjoin(HumanTaskModel).outerjoin( - # HumanTaskUserModel, - # and_( - # HumanTaskModel.id == HumanTaskUserModel.human_task_id, - # HumanTaskUserModel.user_id == user.id, - # ), - # ) - # process_instance_query = process_instance_query.filter( - # or_( - # HumanTaskUserModel.id.is_not(None), - # ProcessInstanceModel.process_initiator_id == user.id, - # ) - # ) - # - # if report_filter.with_tasks_completed_by_me is True: - # process_instance_query = process_instance_query.filter( - # ProcessInstanceModel.process_initiator_id != user.id - # ) - # process_instance_query = process_instance_query.join( - # HumanTaskModel, - # and_( - # HumanTaskModel.process_instance_id == ProcessInstanceModel.id, - # HumanTaskModel.completed_by_user_id == user.id, - # ), - # ) - # - # if report_filter.with_tasks_i_can_complete is True: - # process_instance_query = process_instance_query.filter( - # ProcessInstanceModel.process_initiator_id != user.id - # ) - # process_instance_query = process_instance_query.join( - # HumanTaskModel, - # and_( - # HumanTaskModel.process_instance_id == ProcessInstanceModel.id, - # HumanTaskModel.lane_assignment_id.is_(None), # type: ignore - # ), - # ).join( - # HumanTaskUserModel, - # and_(HumanTaskUserModel.human_task_id == HumanTaskModel.id, HumanTaskUserModel.user_id == user.id), - # ) - # if report_filter.has_active_status: - # process_instance_query = process_instance_query.filter( - # HumanTaskModel.completed.is_(False) # type: ignore - # ) - # - # if report_filter.with_tasks_assigned_to_my_group is True: - # group_model_join_conditions = [GroupModel.id == HumanTaskModel.lane_assignment_id] - # if report_filter.user_group_identifier: - # group_model_join_conditions.append(GroupModel.identifier == report_filter.user_group_identifier) - # - # process_instance_query = process_instance_query.join(HumanTaskModel) - # if report_filter.has_active_status: - # process_instance_query = process_instance_query.filter( - # HumanTaskModel.completed.is_(False) # type: ignore - # ) - # - # process_instance_query = process_instance_query.join(GroupModel, and_(*group_model_join_conditions)) - # process_instance_query = process_instance_query.join( - # UserGroupAssignmentModel, - # UserGroupAssignmentModel.group_id == GroupModel.id, - # ) - # process_instance_query = process_instance_query.filter(UserGroupAssignmentModel.user_id == user.id) - # - # instance_metadata_aliases = {} - # stock_columns = cls.get_column_names_for_model(ProcessInstanceModel) - # if isinstance(report_filter.report_column_list, list): - # process_instance_report.report_metadata["columns"] = report_filter.report_column_list - # if isinstance(report_filter.report_filter_by_list, list): - # process_instance_report.report_metadata["filter_by"] = report_filter.report_filter_by_list - # - # for column in process_instance_report.report_metadata["columns"]: - # if column["accessor"] in stock_columns: - # continue - # instance_metadata_alias = aliased(ProcessInstanceMetadataModel) - # instance_metadata_aliases[column["accessor"]] = instance_metadata_alias - # - # filter_for_column = None - # if "filter_by" in process_instance_report.report_metadata: - # filter_for_column = next( - # ( - # f - # for f in process_instance_report.report_metadata["filter_by"] - # if f["field_name"] == column["accessor"] - # ), - # None, - # ) - # isouter = True - # conditions = [ - # ProcessInstanceModel.id == instance_metadata_alias.process_instance_id, - # instance_metadata_alias.key == column["accessor"], - # ] - # if filter_for_column: - # isouter = False - # conditions.append(instance_metadata_alias.value == filter_for_column["field_value"]) - # process_instance_query = process_instance_query.join( - # instance_metadata_alias, and_(*conditions), isouter=isouter - # ).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"])) - # - # order_by_query_array = [] - # order_by_array = process_instance_report.report_metadata["order_by"] - # if len(order_by_array) < 1: - # order_by_array = ProcessInstanceReportModel.default_order_by() - # for order_by_option in order_by_array: - # attribute = re.sub("^-", "", order_by_option) - # if attribute in stock_columns: - # if order_by_option.startswith("-"): - # order_by_query_array.append(getattr(ProcessInstanceModel, attribute).desc()) - # else: - # order_by_query_array.append(getattr(ProcessInstanceModel, attribute).asc()) - # elif attribute in instance_metadata_aliases: - # if order_by_option.startswith("-"): - # order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).desc()) - # else: - # order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).asc()) + for value in cls.check_filter_value(filters, 'start_from'): + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.start_in_seconds >= value + ) + for value in cls.check_filter_value(filters, 'start_to'): + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.start_in_seconds <= value + ) + for value in cls.check_filter_value(filters, 'end_from'): + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.end_in_seconds >= value + ) + for value in cls.check_filter_value(filters, 'end_to'): + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.end_in_seconds <= value + ) + for value in cls.check_filter_value(filters, 'process_status'): + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.in_(value.split(',')) # type: ignore + ) + + for value in cls.check_filter_value(filters, 'initiated_by_me'): + if value is True: + process_instance_query = process_instance_query.filter_by(process_initiator=user) + + for value in cls.check_filter_value(filters, 'has_terminal_status'): + if value is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore + ) + elif value is False: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.not_in(ProcessInstanceModel.terminal_statuses()) # type: ignore + ) + for value in cls.check_filter_value(filters, 'has_active_status'): + if value is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.in_(ProcessInstanceModel.active_statuses()) # type: ignore + ) + + for value in cls.check_filter_value(filters, 'process_initiator_username'): + initiator = UserModel.query.filter_by(username=value).first() + process_initiator_id = -1 + if initiator: + process_initiator_id = initiator.id + process_instance_query = process_instance_query.filter_by(process_initiator_id=process_initiator_id) + + with_tasks_completed_by_me = cls.get_filter_value(filters, 'with_tasks_completed_by_me') + with_tasks_assigned_to_my_group = cls.get_filter_value(filters, 'with_tasks_assigned_to_my_group') + with_tasks_i_can_complete = cls.get_filter_value(filters, 'with_tasks_i_can_complete') + with_relation_to_me = cls.get_filter_value(filters, 'with_relation_to_me') + has_active_status = cls.get_filter_value(filters, 'has_active_status') + user_group_identifier = cls.get_filter_value(filters, 'user_group_identifier') + if ( + not with_tasks_completed_by_me + and not with_tasks_assigned_to_my_group + and not with_tasks_i_can_complete + and with_relation_to_me is True + ): + process_instance_query = process_instance_query.outerjoin(HumanTaskModel).outerjoin( + HumanTaskUserModel, + and_( + HumanTaskModel.id == HumanTaskUserModel.human_task_id, + HumanTaskUserModel.user_id == user.id, + ), + ) + process_instance_query = process_instance_query.filter( + or_( + HumanTaskUserModel.id.is_not(None), + ProcessInstanceModel.process_initiator_id == user.id, + ) + ) + + if with_tasks_completed_by_me is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.process_initiator_id != user.id + ) + process_instance_query = process_instance_query.join( + HumanTaskModel, + and_( + HumanTaskModel.process_instance_id == ProcessInstanceModel.id, + HumanTaskModel.completed_by_user_id == user.id, + ), + ) + + if with_tasks_i_can_complete is True: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.process_initiator_id != user.id + ) + process_instance_query = process_instance_query.join( + HumanTaskModel, + and_( + HumanTaskModel.process_instance_id == ProcessInstanceModel.id, + HumanTaskModel.lane_assignment_id.is_(None), # type: ignore + ), + ).join( + HumanTaskUserModel, + and_(HumanTaskUserModel.human_task_id == HumanTaskModel.id, HumanTaskUserModel.user_id == user.id), + ) + if has_active_status: + process_instance_query = process_instance_query.filter( + HumanTaskModel.completed.is_(False) # type: ignore + ) + + if with_tasks_assigned_to_my_group is True: + group_model_join_conditions = [GroupModel.id == HumanTaskModel.lane_assignment_id] + if user_group_identifier: + group_model_join_conditions.append(GroupModel.identifier == user_group_identifier) + + process_instance_query = process_instance_query.join(HumanTaskModel) + if has_active_status: + process_instance_query = process_instance_query.filter( + HumanTaskModel.completed.is_(False) # type: ignore + ) + + process_instance_query = process_instance_query.join(GroupModel, and_(*group_model_join_conditions)) + process_instance_query = process_instance_query.join( + UserGroupAssignmentModel, + UserGroupAssignmentModel.group_id == GroupModel.id, + ) + process_instance_query = process_instance_query.filter(UserGroupAssignmentModel.user_id == user.id) + + instance_metadata_aliases = {} + report_column_list = cls.get_filter_value(filters, 'report_column_list') + report_filter_by_list = cls.get_filter_value(filters, 'report_filter_by_list') + stock_columns = cls.get_column_names_for_model(ProcessInstanceModel) + if isinstance(report_column_list, list): + process_instance_report.report_metadata["columns"] = report_column_list + if isinstance(report_filter_by_list, list): + process_instance_report.report_metadata["filter_by"] = report_filter_by_list + + for column in process_instance_report.report_metadata["columns"]: + if column["accessor"] in stock_columns: + continue + instance_metadata_alias = aliased(ProcessInstanceMetadataModel) + instance_metadata_aliases[column["accessor"]] = instance_metadata_alias + + filter_for_column = None + if "filter_by" in process_instance_report.report_metadata: + filter_for_column = next( + ( + f + for f in process_instance_report.report_metadata["filter_by"] + if f["field_name"] == column["accessor"] + ), + None, + ) + isouter = True + conditions = [ + ProcessInstanceModel.id == instance_metadata_alias.process_instance_id, + instance_metadata_alias.key == column["accessor"], + ] + if filter_for_column: + isouter = False + conditions.append(instance_metadata_alias.value == filter_for_column["field_value"]) + process_instance_query = process_instance_query.join( + instance_metadata_alias, and_(*conditions), isouter=isouter + ).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"])) + + order_by_query_array = [] + order_by_array = process_instance_report.report_metadata["order_by"] + if len(order_by_array) < 1: + order_by_array = ProcessInstanceReportModel.default_order_by() + for order_by_option in order_by_array: + attribute = re.sub("^-", "", order_by_option) + if attribute in stock_columns: + if order_by_option.startswith("-"): + order_by_query_array.append(getattr(ProcessInstanceModel, attribute).desc()) + else: + order_by_query_array.append(getattr(ProcessInstanceModel, attribute).asc()) + elif attribute in instance_metadata_aliases: + if order_by_option.startswith("-"): + order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).desc()) + else: + order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).asc()) process_instances = ( process_instance_query.group_by(ProcessInstanceModel.id) .add_columns(ProcessInstanceModel.id) - # .order_by(*order_by_query_array) + .order_by(*order_by_query_array) .paginate(page=page, per_page=per_page, error_out=False) ) results = cls.add_metadata_columns_to_process_instance( process_instances.items, process_instance_report.report_metadata["columns"] ) - # if report_filter.oldest_open_human_task_fields: - # results = cls.add_human_task_fields(results, report_filter.oldest_open_human_task_fields) + for value in cls.check_filter_value(filters, 'oldest_open_human_task_fields'): + results = cls.add_human_task_fields(results, value) response_json = { "report": process_instance_report, "results": results, diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 112957f6..083ce875 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -37,6 +37,7 @@ import { convertSecondsToFormattedDateString, convertSecondsToFormattedDateTime, convertSecondsToFormattedTimeHoursMinutes, + decodeBase64, encodeBase64, getPageInfoFromSearchParams, getProcessModelFullIdentifierFromSearchParams, @@ -287,10 +288,16 @@ export default function ProcessInstanceListTable({ // searchParams.set('key', result.hash); // setSearchParams(searchParams); } - const stopRefreshing = () => { + + // Useful to stop refreshing if an api call gets an error + // since those errors can make the page unusable in any way + const stopRefreshing = (error: any) => { if (clearRefreshRef.current) { clearRefreshRef.current(); } + if (error) { + console.error(error); + } }; function getProcessInstances() { // eslint-disable-next-line prefer-const @@ -306,10 +313,31 @@ export default function ProcessInstanceListTable({ } let queryParamString = `per_page=${perPage}&page=${page}`; - const userAppliedFilter = searchParams.get('user_filter'); - if (userAppliedFilter) { - queryParamString += `&user_filter=${userAppliedFilter}`; - } + // "columns": [ + // {"Header": "id", "accessor": "id"}, + // { + // "Header": "process_model_display_name", + // "accessor": "process_model_display_name", + // }, + // {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, + // {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, + // {"Header": "status", "accessor": "status"}, + // ], + // "filter_by": [ + // {"field_name": "initiated_by_me", "field_value": "true"}, + // {"field_name": "has_terminal_status", "field_value": "true"}, + // ], + // "order_by": ["-start_in_seconds", "-id"], + const postBody: ReportMetadata = { + columns: [], + filter_by: [], + order_by: [], + }; + + // const userAppliedFilter = searchParams.get('user_filter'); + // if (userAppliedFilter) { + // queryParamString += `&user_filter=${userAppliedFilter}`; + // } if (searchParams.get('report_id')) { queryParamString += `&report_id=${searchParams.get('report_id')}`; @@ -318,14 +346,29 @@ export default function ProcessInstanceListTable({ } if (searchParams.get('report_columns')) { - queryParamString += `&report_columns=${searchParams.get( - 'report_columns' - )}`; + // queryParamString += `&report_columns=${searchParams.get( + // 'report_columns' + // )}`; + // const reportColumnsBase64 = encodeBase64(JSON.stringify(reportColumns())); + const reportColumnsBase64 = searchParams.get('report_columns'); + if (reportColumnsBase64) { + const reportColumnsList = JSON.parse( + decodeBase64(reportColumnsBase64) + ); + postBody.columns = reportColumnsList; + } } if (searchParams.get('report_filter_by')) { - queryParamString += `&report_filter_by=${searchParams.get( - 'report_filter_by' - )}`; + // queryParamString += `&report_filter_by=${searchParams.get( + // 'report_filter_by' + // )}`; + const reportFilterByBase64 = searchParams.get('report_filter_by'); + if (reportFilterByBase64) { + const reportFilterByList = JSON.parse( + decodeBase64(reportFilterByBase64) + ); + postBody.filter_by = reportFilterByList; + } } Object.keys(dateParametersToAlwaysFilterBy).forEach( @@ -336,7 +379,11 @@ export default function ProcessInstanceListTable({ dateParametersToAlwaysFilterBy[paramName][1]; const searchParamValue = searchParams.get(paramName); if (searchParamValue) { - queryParamString += `&${paramName}=${searchParamValue}`; + // queryParamString += `&${paramName}=${searchParamValue}`; + postBody.filter_by.push({ + field_name: paramName, + field_value: searchParamValue, + }); const dateString = convertSecondsToFormattedDateString( searchParamValue as any ); @@ -356,11 +403,19 @@ export default function ProcessInstanceListTable({ paramName === 'process_model_identifier' && processModelFullIdentifier ) { - queryParamString += `&process_model_identifier=${processModelFullIdentifier}`; + // queryParamString += `&process_model_identifier=${processModelFullIdentifier}`; + postBody.filter_by.push({ + field_name: 'process_model_identifier', + field_value: processModelFullIdentifier, + }); } else if (searchParams.get(paramName)) { // @ts-expect-error TS(7053) FIXME: const functionToCall = parametersToGetFromSearchParams[paramName]; - queryParamString += `&${paramName}=${searchParams.get(paramName)}`; + // queryParamString += `&${paramName}=${searchParams.get(paramName)}`; + postBody.filter_by.push({ + field_name: paramName, + field_value: searchParams.get(paramName), + }); if (functionToCall !== null) { functionToCall(searchParams.get(paramName) || ''); } @@ -377,8 +432,19 @@ export default function ProcessInstanceListTable({ path: `${processInstanceApiSearchPath}?${queryParamString}`, successCallback: setProcessInstancesFromResult, httpMethod: 'POST', + failureCallback: stopRefreshing, onUnauthorized: stopRefreshing, - postBody: {'report_metadata': {'filter_by': [{'field_name': 'process_model_identifier', 'field_value': 'example/with-milestones' }]}}, + postBody: { + report_metadata: postBody, + // report_metadata: { + // filter_by: [ + // { + // field_name: 'process_model_identifier', + // field_value: 'example/with-milestones', + // }, + // ], + // }, + }, }); } function processResultForProcessModels(result: any) { @@ -618,7 +684,8 @@ export default function ProcessInstanceListTable({ undefined, paginationQueryParamPrefix ); - let queryParamString = `per_page=${perPage}&page=${page}&user_filter=true`; + // let queryParamString = `per_page=${perPage}&page=${page}&user_filter=true`; + let queryParamString = `per_page=${perPage}&page=${page}`; const { valid, startFromSeconds, @@ -631,6 +698,24 @@ export default function ProcessInstanceListTable({ return; } + // "columns": [ + // {"Header": "id", "accessor": "id"}, + // { + // "Header": "process_model_display_name", + // "accessor": "process_model_display_name", + // }, + // {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, + // {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, + // {"Header": "status", "accessor": "status"}, + // ], + // "filter_by": [ + // {"field_name": "initiated_by_me", "field_value": "true"}, + // {"field_name": "has_terminal_status", "field_value": "true"}, + // ], + // "order_by": ["-start_in_seconds", "-id"], + + // const postBody = {}; + if (startFromSeconds) { queryParamString += `&start_from=${startFromSeconds}`; } @@ -947,7 +1032,8 @@ export default function ProcessInstanceListTable({ reportColumnForEditing.accessor ); if (reportFilter) { - reportColumnForEditing.filter_field_value = reportFilter.field_value; + reportColumnForEditing.filter_field_value = + reportFilter.field_value || ''; reportColumnForEditing.filter_operator = reportFilter.operator || 'equals'; } diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index aa0579e9..b3e6bcb2 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -163,7 +163,7 @@ export interface MessageInstance { export interface ReportFilter { field_name: string; - field_value: string; + field_value: string | null; operator?: string; } From 871d7a73deed658311b49926d5b4fe52e3d541a5 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 26 Apr 2023 13:24:08 -0400 Subject: [PATCH 03/18] some more updates for searching w/ burnettk --- .../components/ProcessInstanceListTable.tsx | 235 ++++++++++++------ 1 file changed, 155 insertions(+), 80 deletions(-) diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 083ce875..1652ca36 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -269,8 +269,30 @@ export default function ProcessInstanceListTable({ }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + function setProcessInstancesFromResult(result: any) { + setRequiresRefilter(false); + const processInstancesFromApi = result.results; + setProcessInstances(processInstancesFromApi); + setPagination(result.pagination); + setProcessInstanceFilters(result.filters); + + setReportMetadata(result.report.report_metadata); + if (result.report.id) { + setProcessInstanceReportSelection(result.report); + } + // searchParams.set('key', result.hash); + // setSearchParams(searchParams); + } const clearRefreshRef = useRef(null); + const stopRefreshing = (error: any) => { + if (clearRefreshRef.current) { + clearRefreshRef.current(); + } + if (error) { + console.error(error); + } + }; // eslint-disable-next-line sonarjs/cognitive-complexity useEffect(() => { @@ -428,24 +450,28 @@ export default function ProcessInstanceListTable({ queryParamString += `&${additionalParams}`; } - HttpService.makeCallToBackend({ - path: `${processInstanceApiSearchPath}?${queryParamString}`, - successCallback: setProcessInstancesFromResult, - httpMethod: 'POST', - failureCallback: stopRefreshing, - onUnauthorized: stopRefreshing, - postBody: { - report_metadata: postBody, - // report_metadata: { - // filter_by: [ - // { - // field_name: 'process_model_identifier', - // field_value: 'example/with-milestones', - // }, - // ], - // }, - }, - }); + const reportMetadataBase64 = searchParams.get('report_metadata_base64'); + if (reportMetadataBase64) { + const reportMetadata = JSON.parse(decodeBase64(reportMetadataBase64)) + HttpService.makeCallToBackend({ + path: `${processInstanceApiSearchPath}?${queryParamString}`, + successCallback: setProcessInstancesFromResult, + httpMethod: 'POST', + failureCallback: stopRefreshing, + onUnauthorized: stopRefreshing, + postBody: { + report_metadata: reportMetadata, + // report_metadata: { + // filter_by: [ + // { + // field_name: 'process_model_identifier', + // field_value: 'example/with-milestones', + // }, + // ], + // }, + }, + }); + } } function processResultForProcessModels(result: any) { const processModelFullIdentifierFromSearchParams = @@ -660,7 +686,10 @@ export default function ProcessInstanceListTable({ }; const reportColumns = () => { - return (reportMetadata as any).columns; + if (reportMetadata) { + return (reportMetadata as any).columns; + } + return null; }; const reportFilterBy = () => { @@ -674,6 +703,19 @@ export default function ProcessInstanceListTable({ navigate(`${processInstanceListPathPrefix}?${queryParamString}`); }; + const addFieldValueToReportMetadata = ( + postBody: ReportMetadata, + fieldName: string, + fieldValue: string + ) => { + if (fieldValue) { + postBody.filter_by.push({ + field_name: fieldName, + field_value: fieldValue, + }); + } + }; + const applyFilter = (event: any) => { event.preventDefault(); setProcessInitiatorNotFoundErrorText(''); @@ -685,7 +727,7 @@ export default function ProcessInstanceListTable({ paginationQueryParamPrefix ); // let queryParamString = `per_page=${perPage}&page=${page}&user_filter=true`; - let queryParamString = `per_page=${perPage}&page=${page}`; + const queryParamString = `per_page=${perPage}&page=${page}`; const { valid, startFromSeconds, @@ -713,60 +755,94 @@ export default function ProcessInstanceListTable({ // {"field_name": "has_terminal_status", "field_value": "true"}, // ], // "order_by": ["-start_in_seconds", "-id"], + const postBody: ReportMetadata = { + columns: [], + filter_by: [], + order_by: [], + }; - // const postBody = {}; - - if (startFromSeconds) { - queryParamString += `&start_from=${startFromSeconds}`; - } - if (startToSeconds) { - queryParamString += `&start_to=${startToSeconds}`; - } - if (endFromSeconds) { - queryParamString += `&end_from=${endFromSeconds}`; - } - if (endToSeconds) { - queryParamString += `&end_to=${endToSeconds}`; - } + addFieldValueToReportMetadata(postBody, 'start_from', startFromSeconds); + addFieldValueToReportMetadata(postBody, 'start_to', startToSeconds); + addFieldValueToReportMetadata(postBody, 'end_from', endFromSeconds); + addFieldValueToReportMetadata(postBody, 'end_to', endToSeconds); if (processStatusSelection.length > 0) { - queryParamString += `&process_status=${processStatusSelection}`; + addFieldValueToReportMetadata( + postBody, + 'process_status', + processStatusSelection.join(',') + ); } if (processModelSelection) { - queryParamString += `&process_model_identifier=${processModelSelection.id}`; + addFieldValueToReportMetadata( + postBody, + 'process_model_identifier', + processModelSelection.id + ); } if (processInstanceReportSelection) { - queryParamString += `&report_id=${processInstanceReportSelection.id}`; + addFieldValueToReportMetadata( + postBody, + 'report_id', + processInstanceReportSelection.id.toString() + ); } - const reportColumnsBase64 = encodeBase64(JSON.stringify(reportColumns())); - queryParamString += `&report_columns=${reportColumnsBase64}`; - const reportFilterByBase64 = encodeBase64(JSON.stringify(reportFilterBy())); - queryParamString += `&report_filter_by=${reportFilterByBase64}`; + // console.log('reportColumns()', reportColumns()) + // const reportColumnsBase64 = encodeBase64(JSON.stringify(reportColumns())); + // queryParamString += `&report_columns=${reportColumnsBase64}`; + postBody.columns = reportColumns(); + // console.log('reportFilterBy()', reportFilterBy()) + // const reportFilterByBase64 = encodeBase64(JSON.stringify(reportFilterBy())); + // queryParamString += `&report_filter_by=${reportFilterByBase64}`; if (processInitiatorSelection) { - queryParamString += `&process_initiator_username=${processInitiatorSelection.username}`; - navigateToNewReport(queryParamString); - } else if (processInitiatorText) { - HttpService.makeCallToBackend({ - path: targetUris.userExists, - httpMethod: 'POST', - postBody: { username: processInitiatorText }, - successCallback: (result: any) => { - if (result.user_found) { - queryParamString += `&process_initiator_username=${processInitiatorText}`; - navigateToNewReport(queryParamString); - } else { - setProcessInitiatorNotFoundErrorText( - `The provided username is invalid. Please type the exact username.` - ); - } - }, - }); - } else { - navigateToNewReport(queryParamString); + // queryParamString += `&process_initiator_username=${processInitiatorSelection.username}`; + addFieldValueToReportMetadata( + postBody, + 'process_initiator_username', + processInitiatorSelection.username + ); + // navigateToNewReport(queryParamString); } + // } else if (processInitiatorText) { + // HttpService.makeCallToBackend({ + // path: targetUris.userExists, + // httpMethod: 'POST', + // postBody: { username: processInitiatorText }, + // successCallback: (result: any) => { + // if (result.user_found) { + // queryParamString += `&process_initiator_username=${processInitiatorText}`; + // // navigateToNewReport(queryParamString); + // } else { + // setProcessInitiatorNotFoundErrorText( + // `The provided username is invalid. Please type the exact username.` + // ); + // } + // }, + // }); + // } else { + // navigateToNewReport(queryParamString); + // } + + // http://localhost:7001/admin/process-instances/for-me?per_page=50&page=1&report_metadata_base64=eyJjb2x1bW5zIjpudWxsLCJmaWx0ZXJfYnkiOlt7ImZpZWxkX25hbWUiOiJwcm9jZXNzX3N0YXR1cyIsImZpZWxkX3ZhbHVlIjoiY29tcGxldGUifV0sIm9yZGVyX2J5IjpbXX0%3D + // const queryParamString = `per_page=${perPage}&page=${page}`; + searchParams.set('per_page', perPage.toString()); + searchParams.set('page', page.toString()); + const reportMetadataBase64 = encodeBase64(JSON.stringify(postBody)); + searchParams.set('report_metadata_base64', reportMetadataBase64); + setSearchParams(searchParams); + HttpService.makeCallToBackend({ + path: `${processInstanceApiSearchPath}?${queryParamString}`, + httpMethod: 'POST', + postBody: { report_metadata: postBody }, + failureCallback: stopRefreshing, + onUnauthorized: stopRefreshing, + successCallback: (result: any) => { + setProcessInstancesFromResult(result); + }, + }); }; const dateComponent = ( @@ -1609,6 +1685,7 @@ export default function ProcessInstanceListTable({ return null; }; + let resultsTable = null; if (pagination && (!textToShowIfEmpty || pagination.total > 0)) { // eslint-disable-next-line prefer-const let { page, perPage } = getPageInfoFromSearchParams( @@ -1630,7 +1707,7 @@ export default function ProcessInstanceListTable({

); } - const resultsTable = ( + resultsTable = ( <> {refilterTextComponent} ); - return ( - <> - {reportColumnForm()} - {processInstanceReportSaveTag()} - - {resultsTable} - - ); - } - if (textToShowIfEmpty) { - return ( + } else if (textToShowIfEmpty) { + resultsTable = (

{textToShowIfEmpty}

); } - return null; + return ( + <> + {reportColumnForm()} + {processInstanceReportSaveTag()} + + {resultsTable} + + ); } From 2376080267ce1138ebd0992bbd9ce594745750ed Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 27 Apr 2023 07:28:06 -0400 Subject: [PATCH 04/18] more filter work --- .../src/spiffworkflow_backend/api.yml | 23 + .../routes/process_instances_controller.py | 20 + .../unit/test_process_instance_processor.py | 1 + .../components/ProcessInstanceListTable.tsx | 429 +++++++++--------- 4 files changed, 247 insertions(+), 226 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index e87cc395..653738f4 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1298,6 +1298,29 @@ paths: items: $ref: "#/components/schemas/Workflow" + /process-instances/report-metadata/{report_hash}: + parameters: + - name: report_hash + in: path + required: true + description: The unique id of an existing report + schema: + type: string + get: + operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_report_metadata_show + summary: Returns the metadata associated with a given report hash. + tags: + - Process Instances + responses: + "200": + description: Workflow. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Workflow" + /process-instances/reports/{report_id}: parameters: - name: report_id 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 65821bfb..3009749f 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -1,5 +1,7 @@ """APIs for dealing with process groups, process models, and process instances.""" import base64 +from hashlib import sha256 +from spiffworkflow_backend.models.json_data import JsonDataModel # noqa: F401 import json from typing import Any from typing import Dict @@ -333,9 +335,27 @@ def process_instance_list( user=g.user, ) + json_data_hash = sha256(json.dumps(body['report_metadata'], sort_keys=True).encode("utf8")).hexdigest() + TaskService.insert_or_update_json_data_dict({'hash': json_data_hash, 'data': body['report_metadata']}) + db.session.commit() + # json_data = JsonDataModel.query.filter_by(json_data_hash) + response_json['report_hash'] = json_data_hash + return make_response(jsonify(response_json), 200) +def process_instance_report_metadata_show( + report_hash: str, +) -> flask.wrappers.Response: + json_data = JsonDataModel.query.filter_by(hash=report_hash).first() + if json_data is None: + raise ApiError( + error_code="report_metadata_not_found", + message=f"Could not find report metadata for {report_hash}.", + ) + return make_response(jsonify(json_data.data), 200) + + def process_instance_report_column_list( process_model_identifier: Optional[str] = None, ) -> flask.wrappers.Response: diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py index a6ff70e3..0f410ad6 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py @@ -357,6 +357,7 @@ class TestProcessInstanceProcessor(BaseTest): human_task_one = process_instance.active_human_tasks[0] spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id)) ProcessInstanceService.complete_form_task(processor, spiff_manual_task, {}, initiator_user, human_task_one) + import pdb; pdb.set_trace() assert ( len(process_instance.active_human_tasks) == 0 ), "expected 0 active human tasks after 2nd one is completed" diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 1652ca36..5faaabe9 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -155,6 +155,9 @@ export default function ProcessInstanceListTable({ const [requiresRefilter, setRequiresRefilter] = useState(false); const [lastColumnFilter, setLastColumnFilter] = useState(''); + const [listHasBeenFiltered, setListHasBeenFiltered] = + useState(false); + const preferredUsername = UserService.getPreferredUsername(); const userEmail = UserService.getUserEmail(); @@ -280,8 +283,10 @@ export default function ProcessInstanceListTable({ if (result.report.id) { setProcessInstanceReportSelection(result.report); } - // searchParams.set('key', result.hash); - // setSearchParams(searchParams); + if (result.report_hash) { + searchParams.set('report_hash', result.report_hash); + setSearchParams(searchParams); + } } const clearRefreshRef = useRef(null); @@ -296,6 +301,10 @@ export default function ProcessInstanceListTable({ // eslint-disable-next-line sonarjs/cognitive-complexity useEffect(() => { + // we apparently cannot use a state set in a useEffect from within that same useEffect + // so use a variable instead + let processModelSelectionItemsForUseEffect: ProcessModel[] = []; + function setProcessInstancesFromResult(result: any) { setRequiresRefilter(false); const processInstancesFromApi = result.results; @@ -307,8 +316,6 @@ export default function ProcessInstanceListTable({ if (result.report.id) { setProcessInstanceReportSelection(result.report); } - // searchParams.set('key', result.hash); - // setSearchParams(searchParams); } // Useful to stop refreshing if an api call gets an error @@ -321,7 +328,115 @@ export default function ProcessInstanceListTable({ console.error(error); } }; - function getProcessInstances() { + function getProcessInstances( + reportMetadataBody: ReportMetadata | null = null + ) { + if (listHasBeenFiltered) { + return; + } + let reportMetadataBodyToUse = reportMetadataBody; + if (!reportMetadataBodyToUse) { + reportMetadataBodyToUse = { + columns: [], + filter_by: [], + order_by: [], + }; + } + + let selectedProcessModelIdentifier = processModelFullIdentifier; + reportMetadataBodyToUse.filter_by.forEach( + (reportFilter: ReportFilter) => { + if (reportFilter.field_name === 'process_status') { + setProcessStatusSelection( + (reportFilter.field_value || '').split(',') + ); + } else if (reportFilter.field_name === 'process_model_identifier') { + selectedProcessModelIdentifier = + reportFilter.field_value || undefined; + } else if (dateParametersToAlwaysFilterBy[reportFilter.field_name]) { + const dateFunctionToCall = + dateParametersToAlwaysFilterBy[reportFilter.field_name][0]; + const timeFunctionToCall = + dateParametersToAlwaysFilterBy[reportFilter.field_name][1]; + if (reportFilter.field_value) { + const dateString = convertSecondsToFormattedDateString( + reportFilter.field_value as any + ); + dateFunctionToCall(dateString); + const timeString = convertSecondsToFormattedTimeHoursMinutes( + reportFilter.field_value as any + ); + timeFunctionToCall(timeString); + setShowFilterOptions(true); + } + } + } + ); + + processModelSelectionItemsForUseEffect.forEach( + (processModel: ProcessModel) => { + if (processModel.id === selectedProcessModelIdentifier) { + setProcessModelSelection(processModel); + } + } + ); + + // const postBody: ReportMetadata = { + // columns: [], + // filter_by: [], + // order_by: [], + // }; + // + // if (searchParams.get('report_id')) { + // queryParamString += `&report_id=${searchParams.get('report_id')}`; + // } else if (reportIdentifier) { + // queryParamString += `&report_identifier=${reportIdentifier}`; + // } + + // if (searchParams.get('report_columns')) { + // const reportColumnsBase64 = searchParams.get('report_columns'); + // if (reportColumnsBase64) { + // const reportColumnsList = JSON.parse( + // decodeBase64(reportColumnsBase64) + // ); + // postBody.columns = reportColumnsList; + // } + // } + // if (searchParams.get('report_filter_by')) { + // const reportFilterByBase64 = searchParams.get('report_filter_by'); + // if (reportFilterByBase64) { + // const reportFilterByList = JSON.parse( + // decodeBase64(reportFilterByBase64) + // ); + // postBody.filter_by = reportFilterByList; + // } + // } + // + // Object.keys(parametersToGetFromSearchParams).forEach( + // (paramName: string) => { + // if ( + // paramName === 'process_model_identifier' && + // processModelFullIdentifier + // ) { + // postBody.filter_by.push({ + // field_name: 'process_model_identifier', + // field_value: processModelFullIdentifier, + // }); + // } else if (searchParams.get(paramName)) { + // // @ts-expect-error TS(7053) FIXME: + // const functionToCall = parametersToGetFromSearchParams[paramName]; + // postBody.filter_by.push({ + // field_name: paramName, + // field_value: searchParams.get(paramName), + // }); + // if (functionToCall !== null) { + // functionToCall(searchParams.get(paramName) || ''); + // } + // setShowFilterOptions(true); + // } + // } + // ); + // eslint-disable-next-line prefer-const let { page, perPage } = getPageInfoFromSearchParams( searchParams, @@ -334,172 +449,52 @@ export default function ProcessInstanceListTable({ perPage = perPageOptions[1]; } let queryParamString = `per_page=${perPage}&page=${page}`; - - // "columns": [ - // {"Header": "id", "accessor": "id"}, - // { - // "Header": "process_model_display_name", - // "accessor": "process_model_display_name", - // }, - // {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, - // {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, - // {"Header": "status", "accessor": "status"}, - // ], - // "filter_by": [ - // {"field_name": "initiated_by_me", "field_value": "true"}, - // {"field_name": "has_terminal_status", "field_value": "true"}, - // ], - // "order_by": ["-start_in_seconds", "-id"], - const postBody: ReportMetadata = { - columns: [], - filter_by: [], - order_by: [], - }; - - // const userAppliedFilter = searchParams.get('user_filter'); - // if (userAppliedFilter) { - // queryParamString += `&user_filter=${userAppliedFilter}`; - // } - - if (searchParams.get('report_id')) { - queryParamString += `&report_id=${searchParams.get('report_id')}`; - } else if (reportIdentifier) { - queryParamString += `&report_identifier=${reportIdentifier}`; - } - - if (searchParams.get('report_columns')) { - // queryParamString += `&report_columns=${searchParams.get( - // 'report_columns' - // )}`; - // const reportColumnsBase64 = encodeBase64(JSON.stringify(reportColumns())); - const reportColumnsBase64 = searchParams.get('report_columns'); - if (reportColumnsBase64) { - const reportColumnsList = JSON.parse( - decodeBase64(reportColumnsBase64) - ); - postBody.columns = reportColumnsList; - } - } - if (searchParams.get('report_filter_by')) { - // queryParamString += `&report_filter_by=${searchParams.get( - // 'report_filter_by' - // )}`; - const reportFilterByBase64 = searchParams.get('report_filter_by'); - if (reportFilterByBase64) { - const reportFilterByList = JSON.parse( - decodeBase64(reportFilterByBase64) - ); - postBody.filter_by = reportFilterByList; - } - } - - Object.keys(dateParametersToAlwaysFilterBy).forEach( - (paramName: string) => { - const dateFunctionToCall = - dateParametersToAlwaysFilterBy[paramName][0]; - const timeFunctionToCall = - dateParametersToAlwaysFilterBy[paramName][1]; - const searchParamValue = searchParams.get(paramName); - if (searchParamValue) { - // queryParamString += `&${paramName}=${searchParamValue}`; - postBody.filter_by.push({ - field_name: paramName, - field_value: searchParamValue, - }); - const dateString = convertSecondsToFormattedDateString( - searchParamValue as any - ); - dateFunctionToCall(dateString); - const timeString = convertSecondsToFormattedTimeHoursMinutes( - searchParamValue as any - ); - timeFunctionToCall(timeString); - setShowFilterOptions(true); - } - } - ); - - Object.keys(parametersToGetFromSearchParams).forEach( - (paramName: string) => { - if ( - paramName === 'process_model_identifier' && - processModelFullIdentifier - ) { - // queryParamString += `&process_model_identifier=${processModelFullIdentifier}`; - postBody.filter_by.push({ - field_name: 'process_model_identifier', - field_value: processModelFullIdentifier, - }); - } else if (searchParams.get(paramName)) { - // @ts-expect-error TS(7053) FIXME: - const functionToCall = parametersToGetFromSearchParams[paramName]; - // queryParamString += `&${paramName}=${searchParams.get(paramName)}`; - postBody.filter_by.push({ - field_name: paramName, - field_value: searchParams.get(paramName), - }); - if (functionToCall !== null) { - functionToCall(searchParams.get(paramName) || ''); - } - setShowFilterOptions(true); - } - } - ); - if (additionalParams) { queryParamString += `&${additionalParams}`; } - const reportMetadataBase64 = searchParams.get('report_metadata_base64'); - if (reportMetadataBase64) { - const reportMetadata = JSON.parse(decodeBase64(reportMetadataBase64)) + HttpService.makeCallToBackend({ + path: `${processInstanceApiSearchPath}?${queryParamString}`, + successCallback: setProcessInstancesFromResult, + httpMethod: 'POST', + failureCallback: stopRefreshing, + onUnauthorized: stopRefreshing, + postBody: { + report_metadata: reportMetadataBody, + }, + }); + } + function getReportMetadataWithReportHash() { + if (listHasBeenFiltered) { + return; + } + const reportHash = searchParams.get('report_hash'); + if (reportHash) { HttpService.makeCallToBackend({ - path: `${processInstanceApiSearchPath}?${queryParamString}`, - successCallback: setProcessInstancesFromResult, - httpMethod: 'POST', - failureCallback: stopRefreshing, - onUnauthorized: stopRefreshing, - postBody: { - report_metadata: reportMetadata, - // report_metadata: { - // filter_by: [ - // { - // field_name: 'process_model_identifier', - // field_value: 'example/with-milestones', - // }, - // ], - // }, - }, + path: `/process-instances/report-metadata/${reportHash}`, + successCallback: getProcessInstances, }); + } else { + getProcessInstances(); } } function processResultForProcessModels(result: any) { - const processModelFullIdentifierFromSearchParams = - getProcessModelFullIdentifierFromSearchParams(searchParams); const selectionArray = result.results.map((item: any) => { const label = `${item.id}`; Object.assign(item, { label }); - if (label === processModelFullIdentifierFromSearchParams) { - setProcessModelSelection(item); - } return item; }); + processModelSelectionItemsForUseEffect = selectionArray; setProcessModelAvailableItems(selectionArray); - const processStatusSelectedArray: string[] = []; const processStatusAllOptionsArray = PROCESS_STATUSES.map( (processStatusOption: any) => { - const regex = new RegExp(`\\b${processStatusOption}\\b`); - if ((searchParams.get('process_status') || '').match(regex)) { - processStatusSelectedArray.push(processStatusOption); - } return processStatusOption; } ); - setProcessStatusSelection(processStatusSelectedArray); setProcessStatusAllOptions(processStatusAllOptionsArray); - getProcessInstances(); + getReportMetadataWithReportHash(); } const checkFiltersAndRun = () => { if (filtersEnabled) { @@ -509,7 +504,7 @@ export default function ProcessInstanceListTable({ successCallback: processResultForProcessModels, }); } else { - getProcessInstances(); + getReportMetadataWithReportHash(); } }; @@ -540,59 +535,59 @@ export default function ProcessInstanceListTable({ processInstanceApiSearchPath, ]); - // This sets the filter data using the saved reports returned from the initial instance_list query. - // This could probably be merged into the main useEffect but it works here now. - useEffect(() => { - const filters = processInstanceFilters as any; - Object.keys(dateParametersToAlwaysFilterBy).forEach((paramName: string) => { - const dateFunctionToCall = dateParametersToAlwaysFilterBy[paramName][0]; - const timeFunctionToCall = dateParametersToAlwaysFilterBy[paramName][1]; - const paramValue = filters[paramName]; - dateFunctionToCall(''); - timeFunctionToCall(''); - if (paramValue) { - const dateString = convertSecondsToFormattedDateString( - paramValue as any - ); - dateFunctionToCall(dateString); - const timeString = convertSecondsToFormattedTimeHoursMinutes( - paramValue as any - ); - timeFunctionToCall(timeString); - setShowFilterOptions(true); - } - }); - - setProcessModelSelection(null); - processModelAvailableItems.forEach((item: any) => { - if (item.id === filters.process_model_identifier) { - setProcessModelSelection(item); - } - }); - - 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) => { - const regex = new RegExp(`\\b${processStatusOption}\\b`); - if (filters.process_status.match(regex)) { - processStatusSelectedArray.push(processStatusOption); - } - }); - setShowFilterOptions(true); - } - setProcessStatusSelection(processStatusSelectedArray); - }, [ - processInstanceFilters, - dateParametersToAlwaysFilterBy, - parametersToGetFromSearchParams, - processModelAvailableItems, - ]); + // // This sets the filter data using the saved reports returned from the initial instance_list query. + // // This could probably be merged into the main useEffect but it works here now. + // useEffect(() => { + // const filters = processInstanceFilters as any; + // // Object.keys(dateParametersToAlwaysFilterBy).forEach((paramName: string) => { + // // const dateFunctionToCall = dateParametersToAlwaysFilterBy[paramName][0]; + // // const timeFunctionToCall = dateParametersToAlwaysFilterBy[paramName][1]; + // // const paramValue = filters[paramName]; + // // dateFunctionToCall(''); + // // timeFunctionToCall(''); + // // if (paramValue) { + // // const dateString = convertSecondsToFormattedDateString( + // // paramValue as any + // // ); + // // dateFunctionToCall(dateString); + // // const timeString = convertSecondsToFormattedTimeHoursMinutes( + // // paramValue as any + // // ); + // // timeFunctionToCall(timeString); + // // setShowFilterOptions(true); + // // } + // // }); + // + // // setProcessModelSelection(null); + // // processModelAvailableItems.forEach((item: any) => { + // // if (item.id === filters.process_model_identifier) { + // // setProcessModelSelection(item); + // // } + // // }); + // + // 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) => { + // const regex = new RegExp(`\\b${processStatusOption}\\b`); + // if (filters.process_status.match(regex)) { + // processStatusSelectedArray.push(processStatusOption); + // } + // }); + // setShowFilterOptions(true); + // } + // // setProcessStatusSelection(processStatusSelectedArray); + // }, [ + // processInstanceFilters, + // dateParametersToAlwaysFilterBy, + // parametersToGetFromSearchParams, + // processModelAvailableItems, + // ]); const processInstanceReportSaveTag = () => { if (processInstanceReportJustSaved) { @@ -740,21 +735,6 @@ export default function ProcessInstanceListTable({ return; } - // "columns": [ - // {"Header": "id", "accessor": "id"}, - // { - // "Header": "process_model_display_name", - // "accessor": "process_model_display_name", - // }, - // {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, - // {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, - // {"Header": "status", "accessor": "status"}, - // ], - // "filter_by": [ - // {"field_name": "initiated_by_me", "field_value": "true"}, - // {"field_name": "has_terminal_status", "field_value": "true"}, - // ], - // "order_by": ["-start_in_seconds", "-id"], const postBody: ReportMetadata = { columns: [], filter_by: [], @@ -789,16 +769,9 @@ export default function ProcessInstanceListTable({ ); } - // console.log('reportColumns()', reportColumns()) - // const reportColumnsBase64 = encodeBase64(JSON.stringify(reportColumns())); - // queryParamString += `&report_columns=${reportColumnsBase64}`; postBody.columns = reportColumns(); - // console.log('reportFilterBy()', reportFilterBy()) - // const reportFilterByBase64 = encodeBase64(JSON.stringify(reportFilterBy())); - // queryParamString += `&report_filter_by=${reportFilterByBase64}`; if (processInitiatorSelection) { - // queryParamString += `&process_initiator_username=${processInitiatorSelection.username}`; addFieldValueToReportMetadata( postBody, 'process_initiator_username', @@ -828,11 +801,15 @@ export default function ProcessInstanceListTable({ // http://localhost:7001/admin/process-instances/for-me?per_page=50&page=1&report_metadata_base64=eyJjb2x1bW5zIjpudWxsLCJmaWx0ZXJfYnkiOlt7ImZpZWxkX25hbWUiOiJwcm9jZXNzX3N0YXR1cyIsImZpZWxkX3ZhbHVlIjoiY29tcGxldGUifV0sIm9yZGVyX2J5IjpbXX0%3D // const queryParamString = `per_page=${perPage}&page=${page}`; + + setListHasBeenFiltered(true); searchParams.set('per_page', perPage.toString()); searchParams.set('page', page.toString()); - const reportMetadataBase64 = encodeBase64(JSON.stringify(postBody)); - searchParams.set('report_metadata_base64', reportMetadataBase64); + + // const reportMetadataBase64 = encodeBase64(JSON.stringify(postBody)); + // searchParams.set('report_metadata_base64', reportMetadataBase64); setSearchParams(searchParams); + HttpService.makeCallToBackend({ path: `${processInstanceApiSearchPath}?${queryParamString}`, httpMethod: 'POST', From e49734b9ef9bdcedefa4a3d8a47a619e6f9b342f Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 27 Apr 2023 11:47:48 -0400 Subject: [PATCH 05/18] most of the process instance filters are working. still need to add back in support for reports w/ burnettk --- .../process_instance_report_service.py | 63 ++++--- .../components/ProcessInstanceListTable.tsx | 157 +++++++----------- 2 files changed, 102 insertions(+), 118 deletions(-) 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 92040b2b..ab180fee 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 @@ -412,11 +412,16 @@ class ProcessInstanceReportService: ) -> list[dict]: """Add_metadata_columns_to_process_instance.""" results = [] + non_metadata_columns = cls.non_metadata_columns() for process_instance_row in process_instance_sqlalchemy_rows: process_instance_mapping = process_instance_row._mapping process_instance_dict = process_instance_row[0].serialized + # import pdb; pdb.set_trace() for metadata_column in metadata_columns: + # if metadata_column["accessor"] not in non_metadata_columns: + # if metadata_column["accessor"] not in cls.process_instance_stock_columns(): if metadata_column["accessor"] not in process_instance_dict: + # import pdb; pdb.set_trace() process_instance_dict[metadata_column["accessor"]] = process_instance_mapping[ metadata_column["accessor"] ] @@ -477,6 +482,14 @@ class ProcessInstanceReportService: """Get_column_names_for_model.""" return [i.name for i in model.__table__.columns] + @classmethod + def process_instance_stock_columns(cls) -> list[str]: + return cls.get_column_names_for_model(ProcessInstanceModel) + + @classmethod + def non_metadata_columns(cls) -> list[str]: + return cls.process_instance_stock_columns() + ['process_initiator_username'] + @classmethod def builtin_column_options(cls) -> list[dict]: """Builtin_column_options.""" @@ -663,16 +676,15 @@ class ProcessInstanceReportService: process_instance_query = process_instance_query.filter(UserGroupAssignmentModel.user_id == user.id) instance_metadata_aliases = {} - report_column_list = cls.get_filter_value(filters, 'report_column_list') - report_filter_by_list = cls.get_filter_value(filters, 'report_filter_by_list') - stock_columns = cls.get_column_names_for_model(ProcessInstanceModel) - if isinstance(report_column_list, list): - process_instance_report.report_metadata["columns"] = report_column_list - if isinstance(report_filter_by_list, list): - process_instance_report.report_metadata["filter_by"] = report_filter_by_list + # print(f"report_metadata['columns']: {report_metadata['columns']}") + # import pdb; pdb.set_trace() + if report_metadata['columns'] and len(report_metadata['columns']) > 0: + process_instance_report.report_metadata["columns"] = report_metadata['columns'] + if report_metadata['filter_by'] and len(report_metadata['filter_by']) > 0: + process_instance_report.report_metadata["filter_by"] = report_metadata['filter_by'] for column in process_instance_report.report_metadata["columns"]: - if column["accessor"] in stock_columns: + if column["accessor"] in cls.non_metadata_columns(): continue instance_metadata_alias = aliased(ProcessInstanceMetadataModel) instance_metadata_aliases[column["accessor"]] = instance_metadata_alias @@ -699,32 +711,33 @@ class ProcessInstanceReportService: instance_metadata_alias, and_(*conditions), isouter=isouter ).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"])) - order_by_query_array = [] - order_by_array = process_instance_report.report_metadata["order_by"] - if len(order_by_array) < 1: - order_by_array = ProcessInstanceReportModel.default_order_by() - for order_by_option in order_by_array: - attribute = re.sub("^-", "", order_by_option) - if attribute in stock_columns: - if order_by_option.startswith("-"): - order_by_query_array.append(getattr(ProcessInstanceModel, attribute).desc()) - else: - order_by_query_array.append(getattr(ProcessInstanceModel, attribute).asc()) - elif attribute in instance_metadata_aliases: - if order_by_option.startswith("-"): - order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).desc()) - else: - order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).asc()) + # order_by_query_array = [] + # order_by_array = process_instance_report.report_metadata["order_by"] + # if len(order_by_array) < 1: + # order_by_array = ProcessInstanceReportModel.default_order_by() + # for order_by_option in order_by_array: + # attribute = re.sub("^-", "", order_by_option) + # if attribute in cls.process_instance_stock_columns(): + # if order_by_option.startswith("-"): + # order_by_query_array.append(getattr(ProcessInstanceModel, attribute).desc()) + # else: + # order_by_query_array.append(getattr(ProcessInstanceModel, attribute).asc()) + # elif attribute in instance_metadata_aliases: + # if order_by_option.startswith("-"): + # order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).desc()) + # else: + # order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).asc()) process_instances = ( process_instance_query.group_by(ProcessInstanceModel.id) .add_columns(ProcessInstanceModel.id) - .order_by(*order_by_query_array) + # .order_by(*order_by_query_array) .paginate(page=page, per_page=per_page, error_out=False) ) results = cls.add_metadata_columns_to_process_instance( process_instances.items, process_instance_report.report_metadata["columns"] ) + # import pdb; pdb.set_trace() for value in cls.check_filter_value(filters, 'oldest_open_human_task_fields'): results = cls.add_human_task_fields(results, value) diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 9b521e35..85de9ba9 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -128,7 +128,9 @@ export default function ProcessInstanceListTable({ const permissionRequestData: PermissionsToCheck = { [targetUris.userSearch]: ['GET'], }; - const { ability } = usePermissionFetcher(permissionRequestData); + const { ability, permissionsLoaded } = usePermissionFetcher( + permissionRequestData + ); const canSearchUsers: boolean = ability.can('GET', targetUris.userSearch); const [processInstances, setProcessInstances] = useState([]); @@ -301,6 +303,10 @@ export default function ProcessInstanceListTable({ // eslint-disable-next-line sonarjs/cognitive-complexity useEffect(() => { + if (!permissionsLoaded) { + return; + } + // we apparently cannot use a state set in a useEffect from within that same useEffect // so use a variable instead let processModelSelectionItemsForUseEffect: ProcessModel[] = []; @@ -311,7 +317,6 @@ export default function ProcessInstanceListTable({ setProcessInstances(processInstancesFromApi); setPagination(result.pagination); setProcessInstanceFilters(result.filters); - setReportMetadata(result.report.report_metadata); if (result.report.id) { setProcessInstanceReportSelection(result.report); @@ -350,6 +355,10 @@ export default function ProcessInstanceListTable({ setProcessStatusSelection( (reportFilter.field_value || '').split(',') ); + setShowFilterOptions(true); + } else if (reportFilter.field_name === 'process_initiator_username') { + searchForProcessInitiator(reportFilter.field_value || ''); + setShowFilterOptions(true); } else if (reportFilter.field_name === 'process_model_identifier') { selectedProcessModelIdentifier = reportFilter.field_value || undefined; @@ -377,16 +386,11 @@ export default function ProcessInstanceListTable({ (processModel: ProcessModel) => { if (processModel.id === selectedProcessModelIdentifier) { setProcessModelSelection(processModel); + setShowFilterOptions(true); } } ); - // const postBody: ReportMetadata = { - // columns: [], - // filter_by: [], - // order_by: [], - // }; - // // if (searchParams.get('report_id')) { // queryParamString += `&report_id=${searchParams.get('report_id')}`; // } else if (reportIdentifier) { @@ -402,40 +406,6 @@ export default function ProcessInstanceListTable({ // postBody.columns = reportColumnsList; // } // } - // if (searchParams.get('report_filter_by')) { - // const reportFilterByBase64 = searchParams.get('report_filter_by'); - // if (reportFilterByBase64) { - // const reportFilterByList = JSON.parse( - // decodeBase64(reportFilterByBase64) - // ); - // postBody.filter_by = reportFilterByList; - // } - // } - // - // Object.keys(parametersToGetFromSearchParams).forEach( - // (paramName: string) => { - // if ( - // paramName === 'process_model_identifier' && - // processModelFullIdentifier - // ) { - // postBody.filter_by.push({ - // field_name: 'process_model_identifier', - // field_value: processModelFullIdentifier, - // }); - // } else if (searchParams.get(paramName)) { - // // @ts-expect-error TS(7053) FIXME: - // const functionToCall = parametersToGetFromSearchParams[paramName]; - // postBody.filter_by.push({ - // field_name: paramName, - // field_value: searchParams.get(paramName), - // }); - // if (functionToCall !== null) { - // functionToCall(searchParams.get(paramName) || ''); - // } - // setShowFilterOptions(true); - // } - // } - // ); // eslint-disable-next-line prefer-const let { page, perPage } = getPageInfoFromSearchParams( @@ -460,7 +430,7 @@ export default function ProcessInstanceListTable({ failureCallback: stopRefreshing, onUnauthorized: stopRefreshing, postBody: { - report_metadata: reportMetadataBody, + report_metadata: reportMetadataBodyToUse, }, }); } @@ -533,6 +503,7 @@ export default function ProcessInstanceListTable({ reportIdentifier, additionalParams, processInstanceApiSearchPath, + permissionsLoaded, ]); // // This sets the filter data using the saved reports returned from the initial instance_list query. @@ -682,14 +653,17 @@ export default function ProcessInstanceListTable({ const reportColumns = () => { if (reportMetadata) { - return (reportMetadata as any).columns; + return reportMetadata.columns; } - return null; + return []; }; - const reportFilterBy = () => { - return (reportMetadata as any).filter_by; - }; + // const reportFilterBy = () => { + // if (reportMetadata) { + // return reportMetadata.filter_by; + // } + // return null + // }; const navigateToNewReport = (queryParamString: string) => { removeError(); @@ -698,6 +672,16 @@ export default function ProcessInstanceListTable({ navigate(`${processInstanceListPathPrefix}?${queryParamString}`); }; + const removeFieldFromReportMetadata = ( + postBody: ReportMetadata, + fieldName: string + ) => { + const filtersToKeep = postBody.filter_by.filter( + (rf: ReportFilter) => rf.field_name !== fieldName + ); + postBody.filter_by = filtersToKeep; + }; + const addFieldValueToReportMetadata = ( postBody: ReportMetadata, fieldName: string, @@ -708,6 +692,8 @@ export default function ProcessInstanceListTable({ field_name: fieldName, field_value: fieldValue, }); + } else { + removeFieldFromReportMetadata(postBody, fieldName); } }; @@ -721,8 +707,6 @@ export default function ProcessInstanceListTable({ undefined, paginationQueryParamPrefix ); - // let queryParamString = `per_page=${perPage}&page=${page}&user_filter=true`; - const queryParamString = `per_page=${perPage}&page=${page}`; const { valid, startFromSeconds, @@ -735,11 +719,17 @@ export default function ProcessInstanceListTable({ return; } - const postBody: ReportMetadata = { - columns: [], - filter_by: [], - order_by: [], - }; + let postBody = null; + if (reportMetadata) { + postBody = { ...reportMetadata }; + } + if (!postBody) { + postBody = { + columns: [], + filter_by: [], + order_by: [], + }; + } addFieldValueToReportMetadata(postBody, 'start_from', startFromSeconds); addFieldValueToReportMetadata(postBody, 'start_to', startToSeconds); @@ -751,6 +741,8 @@ export default function ProcessInstanceListTable({ 'process_status', processStatusSelection.join(',') ); + } else { + removeFieldFromReportMetadata(postBody, 'process_status'); } if (processModelSelection) { @@ -759,17 +751,17 @@ export default function ProcessInstanceListTable({ 'process_model_identifier', processModelSelection.id ); + } else { + removeFieldFromReportMetadata(postBody, 'process_model_identifier'); } - if (processInstanceReportSelection) { - addFieldValueToReportMetadata( - postBody, - 'report_id', - processInstanceReportSelection.id.toString() - ); - } - - postBody.columns = reportColumns(); + // if (processInstanceReportSelection) { + // addFieldValueToReportMetadata( + // postBody, + // 'report_id', + // processInstanceReportSelection.id.toString() + // ); + // } if (processInitiatorSelection) { addFieldValueToReportMetadata( @@ -777,39 +769,17 @@ export default function ProcessInstanceListTable({ 'process_initiator_username', processInitiatorSelection.username ); - // navigateToNewReport(queryParamString); + } else { + removeFieldFromReportMetadata(postBody, 'process_initiator_username'); } - // } else if (processInitiatorText) { - // HttpService.makeCallToBackend({ - // path: targetUris.userExists, - // httpMethod: 'POST', - // postBody: { username: processInitiatorText }, - // successCallback: (result: any) => { - // if (result.user_found) { - // queryParamString += `&process_initiator_username=${processInitiatorText}`; - // // navigateToNewReport(queryParamString); - // } else { - // setProcessInitiatorNotFoundErrorText( - // `The provided username is invalid. Please type the exact username.` - // ); - // } - // }, - // }); - // } else { - // navigateToNewReport(queryParamString); - // } - - // http://localhost:7001/admin/process-instances/for-me?per_page=50&page=1&report_metadata_base64=eyJjb2x1bW5zIjpudWxsLCJmaWx0ZXJfYnkiOlt7ImZpZWxkX25hbWUiOiJwcm9jZXNzX3N0YXR1cyIsImZpZWxkX3ZhbHVlIjoiY29tcGxldGUifV0sIm9yZGVyX2J5IjpbXX0%3D - // const queryParamString = `per_page=${perPage}&page=${page}`; setListHasBeenFiltered(true); + setReportMetadata(postBody); searchParams.set('per_page', perPage.toString()); searchParams.set('page', page.toString()); - - // const reportMetadataBase64 = encodeBase64(JSON.stringify(postBody)); - // searchParams.set('report_metadata_base64', reportMetadataBase64); setSearchParams(searchParams); + const queryParamString = `per_page=${perPage}&page=${page}`; HttpService.makeCallToBackend({ path: `${processInstanceApiSearchPath}?${queryParamString}`, httpMethod: 'POST', @@ -1081,6 +1051,7 @@ export default function ProcessInstanceListTable({ reportColumn, { filter_field_value: '', filter_operator: '' } ); + if (reportColumn.filterable) { const reportFilter = getFilterByFromReportMetadata( reportColumnForEditing.accessor ); @@ -1090,6 +1061,7 @@ export default function ProcessInstanceListTable({ reportColumnForEditing.filter_operator = reportFilter.operator || 'equals'; } + } return reportColumnForEditing; }; @@ -1212,10 +1184,9 @@ export default function ProcessInstanceListTable({ }; const columnSelections = () => { - if (reportColumns()) { + if (reportColumns().length > 0) { const tags: any = []; - - (reportColumns() as any).forEach((reportColumn: ReportColumn) => { + reportColumns().forEach((reportColumn: ReportColumn) => { const reportColumnForEditing = reportColumnToReportColumnForEditing(reportColumn); From b86ddf8a96454b25955b44b7a5d9be076e255691 Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 27 Apr 2023 15:19:02 -0400 Subject: [PATCH 06/18] reports seem to be working again w/ burnettk --- .../migrations/versions/68adb1d504e1_.py | 34 +++ .../src/spiffworkflow_backend/api.yml | 52 ++-- .../spiffworkflow_backend/models/json_data.py | 37 +++ .../models/process_instance_report.py | 9 + .../routes/process_instances_controller.py | 105 ++++--- .../routes/tasks_controller.py | 5 +- .../services/process_instance_processor.py | 2 +- .../services/task_service.py | 30 +- .../ProcessInstanceListSaveAsReport.tsx | 140 +++++---- .../components/ProcessInstanceListTable.tsx | 265 +++++++++--------- .../ProcessInstanceReportSearch.tsx | 19 +- .../src/routes/AdminRoutes.tsx | 5 - .../src/routes/ProcessInstanceReportShow.tsx | 97 ------- 13 files changed, 401 insertions(+), 399 deletions(-) create mode 100644 spiffworkflow-backend/migrations/versions/68adb1d504e1_.py delete mode 100644 spiffworkflow-frontend/src/routes/ProcessInstanceReportShow.tsx diff --git a/spiffworkflow-backend/migrations/versions/68adb1d504e1_.py b/spiffworkflow-backend/migrations/versions/68adb1d504e1_.py new file mode 100644 index 00000000..dbeca2ba --- /dev/null +++ b/spiffworkflow-backend/migrations/versions/68adb1d504e1_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 68adb1d504e1 +Revises: 0c7428378d6e +Create Date: 2023-04-27 12:24:01.771698 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '68adb1d504e1' +down_revision = '0c7428378d6e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('process_instance_report', schema=None) as batch_op: + batch_op.add_column(sa.Column('json_data_hash', sa.String(length=255), nullable=False)) + batch_op.create_index(batch_op.f('ix_process_instance_report_json_data_hash'), ['json_data_hash'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('process_instance_report', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_process_instance_report_json_data_hash')) + batch_op.drop_column('json_data_hash') + + # ### end Alembic commands ### diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 2ae0c4a7..27172a0f 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1298,17 +1298,29 @@ paths: items: $ref: "#/components/schemas/Workflow" - /process-instances/report-metadata/{report_hash}: + /process-instances/report-metadata: parameters: - name: report_hash - in: path - required: true - description: The unique id of an existing report + in: query + required: false + description: The hash of a query that has been searched before. + schema: + type: string + - name: report_id + in: query + required: false + description: The unique id of an existing report. + schema: + type: integer + - name: report_identifier + in: query + required: false + description: Specifies the identifier of a report to use, if any schema: type: string get: - operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_report_metadata_show - summary: Returns the metadata associated with a given report hash. + operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_report_show + summary: Returns the metadata associated with a given report key. This favors report_hash over report_id and report_identifier. tags: - Process Instances responses: @@ -1341,20 +1353,20 @@ paths: description: The page number to return. Defaults to page 1. schema: type: integer - get: - operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_report_show - summary: Returns a report of process instances for a given process model - tags: - - Process Instances - responses: - "200": - description: Workflow. - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/Workflow" + # get: + # operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_report_show + # summary: Returns a report of process instances for a given process model + # tags: + # - Process Instances + # responses: + # "200": + # description: Workflow. + # content: + # application/json: + # schema: + # type: array + # items: + # $ref: "#/components/schemas/Workflow" put: operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_report_update summary: Updates a process instance report diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/json_data.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/json_data.py index 3c0aed85..5c094b86 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/json_data.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/json_data.py @@ -1,4 +1,10 @@ from __future__ import annotations +import json +from hashlib import sha256 +from flask import current_app +from typing import TypedDict +from sqlalchemy.dialects.mysql import insert as mysql_insert +from sqlalchemy.dialects.postgresql import insert as postgres_insert from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel @@ -8,6 +14,11 @@ class JsonDataModelNotFoundError(Exception): pass +class JsonDataDict(TypedDict): + hash: str + data: dict + + # delta algorithm <- just to save it for when we want to try to implement it: # a = {"hey": { "hey2": 2, "hey3": 3, "hey6": 7 }, "hey30": 3, "hey40": 4} # b = {"hey": { "hey2": 4, "hey5": 3 }, "hey20": 2, "hey30": 3} @@ -42,3 +53,29 @@ class JsonDataModel(SpiffworkflowBaseDBModel): @classmethod def find_data_dict_by_hash(cls, hash: str) -> dict: return cls.find_object_by_hash(hash).data + + @classmethod + def insert_or_update_json_data_records( + cls, json_data_hash_to_json_data_dict_mapping: dict[str, JsonDataDict] + ) -> None: + list_of_dicts = [*json_data_hash_to_json_data_dict_mapping.values()] + if len(list_of_dicts) > 0: + on_duplicate_key_stmt = None + if current_app.config["SPIFFWORKFLOW_BACKEND_DATABASE_TYPE"] == "mysql": + insert_stmt = mysql_insert(JsonDataModel).values(list_of_dicts) + on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(data=insert_stmt.inserted.data) + else: + insert_stmt = postgres_insert(JsonDataModel).values(list_of_dicts) + on_duplicate_key_stmt = insert_stmt.on_conflict_do_nothing(index_elements=["hash"]) + db.session.execute(on_duplicate_key_stmt) + + @classmethod + def insert_or_update_json_data_dict(cls, json_data_dict: JsonDataDict) -> None: + cls.insert_or_update_json_data_records({json_data_dict["hash"]: json_data_dict}) + + @classmethod + def create_and_insert_json_data_from_dict(cls, data: dict) -> str: + json_data_hash = sha256(json.dumps(data, sort_keys=True).encode("utf8")).hexdigest() + cls.insert_or_update_json_data_dict({'hash': json_data_hash, 'data': data}) + db.session.commit() + return json_data_hash diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py index 2c8a442d..25497b1c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py @@ -1,5 +1,6 @@ """Process_instance.""" from __future__ import annotations +from spiffworkflow_backend.models.json_data import JsonDataModel # noqa: F401 from dataclasses import dataclass from typing import Any @@ -88,6 +89,11 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): created_at_in_seconds = db.Column(db.Integer) updated_at_in_seconds = db.Column(db.Integer) + json_data_hash: str = db.Column(db.String(255), nullable=False, index=True) + + def get_report_metadata(self) -> dict: + return JsonDataModel.find_data_dict_by_hash(self.json_data_hash) + @classmethod def default_order_by(cls) -> list[str]: """Default_order_by.""" @@ -154,10 +160,13 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): f"Process instance report with identifier already exists: {identifier}" ) + json_data_hash = JsonDataModel.create_and_insert_json_data_from_dict(report_metadata) + process_instance_report = cls( identifier=identifier, created_by_id=user.id, report_metadata=report_metadata, + json_data_hash=json_data_hash, ) db.session.add(process_instance_report) db.session.commit() 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 3009749f..a9471c9a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -335,25 +335,42 @@ def process_instance_list( user=g.user, ) - json_data_hash = sha256(json.dumps(body['report_metadata'], sort_keys=True).encode("utf8")).hexdigest() - TaskService.insert_or_update_json_data_dict({'hash': json_data_hash, 'data': body['report_metadata']}) - db.session.commit() - # json_data = JsonDataModel.query.filter_by(json_data_hash) + json_data_hash = JsonDataModel.create_and_insert_json_data_from_dict(body['report_metadata']) response_json['report_hash'] = json_data_hash + db.session.commit() return make_response(jsonify(response_json), 200) -def process_instance_report_metadata_show( - report_hash: str, +def process_instance_report_show( + report_hash: Optional[str] = None, + report_id: Optional[int] = None, + report_identifier: Optional[str] = None, ) -> flask.wrappers.Response: - json_data = JsonDataModel.query.filter_by(hash=report_hash).first() - if json_data is None: + + if report_hash is None and report_id is None and report_identifier is None: raise ApiError( - error_code="report_metadata_not_found", - message=f"Could not find report metadata for {report_hash}.", + error_code="report_key_missing", + message="A report key is needed to lookup a report. Either choose a report_hash, report_id, or report_identifier.", ) - return make_response(jsonify(json_data.data), 200) + response_result = {} + if report_hash is not None: + json_data = JsonDataModel.query.filter_by(hash=report_hash).first() + if json_data is None: + raise ApiError( + error_code="report_metadata_not_found", + message=f"Could not find report metadata for {report_hash}.", + ) + response_result = { + "id": 0, + "identifier": "custom", + "name": "custom", + "report_metadata": json_data.data, + } + else: + response_result = ProcessInstanceReportService.report_with_identifier(g.user, report_id, report_identifier) + + return make_response(jsonify(response_result), 200) def process_instance_report_column_list( @@ -490,39 +507,39 @@ def process_instance_report_delete( return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") -def process_instance_report_show( - report_id: int, - page: int = 1, - per_page: int = 100, -) -> flask.wrappers.Response: - """Process_instance_report_show.""" - process_instances = ProcessInstanceModel.query.order_by( - ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore - ).paginate(page=page, per_page=per_page, error_out=False) - - process_instance_report = ProcessInstanceReportModel.query.filter_by( - id=report_id, - created_by_id=g.user.id, - ).first() - if process_instance_report is None: - raise ApiError( - error_code="unknown_process_instance_report", - message="Unknown process instance report", - status_code=404, - ) - - substitution_variables = request.args.to_dict() - result_dict = process_instance_report.generate_report(process_instances.items, substitution_variables) - - # update this if we go back to a database query instead of filtering in memory - result_dict["pagination"] = { - "count": len(result_dict["results"]), - "total": len(result_dict["results"]), - "pages": 1, - } - - return Response(json.dumps(result_dict), status=200, mimetype="application/json") - +# def process_instance_report_show( +# report_id: int, +# page: int = 1, +# per_page: int = 100, +# ) -> flask.wrappers.Response: +# """Process_instance_report_show.""" +# process_instances = ProcessInstanceModel.query.order_by( +# ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore +# ).paginate(page=page, per_page=per_page, error_out=False) +# +# process_instance_report = ProcessInstanceReportModel.query.filter_by( +# id=report_id, +# created_by_id=g.user.id, +# ).first() +# if process_instance_report is None: +# raise ApiError( +# error_code="unknown_process_instance_report", +# message="Unknown process instance report", +# status_code=404, +# ) +# +# substitution_variables = request.args.to_dict() +# result_dict = process_instance_report.generate_report(process_instances.items, substitution_variables) +# +# # update this if we go back to a database query instead of filtering in memory +# result_dict["pagination"] = { +# "count": len(result_dict["results"]), +# "total": len(result_dict["results"]), +# "pages": 1, +# } +# +# return Response(json.dumps(result_dict), status=200, mimetype="application/json") +# def process_instance_task_list_without_task_data_for_me( modified_process_model_identifier: str, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index 64c22c5b..b62cd1c1 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -1,5 +1,6 @@ """APIs for dealing with process groups, process models, and process instances.""" import json +from spiffworkflow_backend.models.json_data import JsonDataModel # noqa: F401 import os import uuid from sys import exc_info @@ -220,7 +221,7 @@ def task_data_update( task_model, new_task_data_dict, "json_data_hash" ) if json_data_dict is not None: - TaskService.insert_or_update_json_data_records({json_data_dict["hash"]: json_data_dict}) + JsonDataModel.insert_or_update_json_data_records({json_data_dict["hash"]: json_data_dict}) ProcessInstanceTmpService.add_event_to_process_instance( process_instance, ProcessInstanceEventType.task_data_edited.value, task_guid=task_guid ) @@ -537,7 +538,7 @@ def _task_submit_shared( task_model, spiff_task.data, "json_data_hash" ) if json_data_dict is not None: - TaskService.insert_or_update_json_data_dict(json_data_dict) + JsonDataModel.insert_or_update_json_data_dict(json_data_dict) db.session.add(task_model) db.session.commit() else: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index 45d27509..39d37c2c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -1737,7 +1737,7 @@ class ProcessInstanceProcessor: bpmn_definition_to_task_definitions_mappings=self.bpmn_definition_to_task_definitions_mappings, ) task_service.update_task_model(task_model, spiff_task) - TaskService.insert_or_update_json_data_records(task_service.json_data_dicts) + JsonDataModel.insert_or_update_json_data_records(task_service.json_data_dicts) ProcessInstanceTmpService.add_event_to_process_instance( self.process_instance_model, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py index 14441475..ec088dd3 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py @@ -15,14 +15,12 @@ from SpiffWorkflow.exceptions import WorkflowException # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import TaskState from SpiffWorkflow.task import TaskStateNames -from sqlalchemy.dialects.mysql import insert as mysql_insert -from sqlalchemy.dialects.postgresql import insert as postgres_insert from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel from spiffworkflow_backend.models.bpmn_process import BpmnProcessNotFoundError from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel from spiffworkflow_backend.models.db import db -from spiffworkflow_backend.models.json_data import JsonDataModel # noqa: F401 +from spiffworkflow_backend.models.json_data import JsonDataDict, JsonDataModel # noqa: F401 from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType @@ -38,11 +36,6 @@ class StartAndEndTimes(TypedDict): end_in_seconds: Optional[float] -class JsonDataDict(TypedDict): - hash: str - data: dict - - class TaskModelError(Exception): """Copied from SpiffWorkflow.exceptions.WorkflowTaskException. @@ -130,7 +123,7 @@ class TaskService: db.session.bulk_save_objects(self.bpmn_processes.values()) db.session.bulk_save_objects(self.task_models.values()) db.session.bulk_save_objects(self.process_instance_events.values()) - self.__class__.insert_or_update_json_data_records(self.json_data_dicts) + JsonDataModel.insert_or_update_json_data_records(self.json_data_dicts) def process_parents_and_children_and_save_to_database( self, @@ -483,10 +476,6 @@ class TaskService: bpmn_process.json_data_hash = bpmn_process_data_hash return json_data_dict - @classmethod - def insert_or_update_json_data_dict(cls, json_data_dict: JsonDataDict) -> None: - TaskService.insert_or_update_json_data_records({json_data_dict["hash"]: json_data_dict}) - @classmethod def update_task_data_on_task_model_and_return_dict_if_updated( cls, task_model: TaskModel, task_data_dict: dict, task_model_data_column: str @@ -610,21 +599,6 @@ class TaskService: new_properties_json["state"] = getattr(TaskState, state) task_model.properties_json = new_properties_json - @classmethod - def insert_or_update_json_data_records( - cls, json_data_hash_to_json_data_dict_mapping: dict[str, JsonDataDict] - ) -> None: - list_of_dicts = [*json_data_hash_to_json_data_dict_mapping.values()] - if len(list_of_dicts) > 0: - on_duplicate_key_stmt = None - if current_app.config["SPIFFWORKFLOW_BACKEND_DATABASE_TYPE"] == "mysql": - insert_stmt = mysql_insert(JsonDataModel).values(list_of_dicts) - on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(data=insert_stmt.inserted.data) - else: - insert_stmt = postgres_insert(JsonDataModel).values(list_of_dicts) - on_duplicate_key_stmt = insert_stmt.on_conflict_do_nothing(index_elements=["hash"]) - db.session.execute(on_duplicate_key_stmt) - @classmethod def get_extensions_from_task_model(cls, task_model: TaskModel) -> dict: task_definition = task_model.task_definition diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx index 6953a20c..78186d97 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx @@ -75,73 +75,73 @@ export default function ProcessInstanceListSaveAsReport({ const addProcessInstanceReport = (event: any) => { event.preventDefault(); - // TODO: make a field to set this - let orderByArray = ['-start_in_seconds', '-id']; - if (orderBy) { - orderByArray = orderBy.split(',').filter((n) => n); - } - const filterByArray: any = []; - - if (processModelSelection) { - filterByArray.push({ - field_name: 'process_model_identifier', - field_value: processModelSelection.id, - }); - } - - if (processInitiatorSelection) { - filterByArray.push({ - field_name: 'process_initiator_username', - field_value: processInitiatorSelection.username, - }); - } - - if (processStatusSelection.length > 0) { - filterByArray.push({ - field_name: 'process_status', - field_value: processStatusSelection.join(','), - operator: 'in', - }); - } - - if (startFromSeconds) { - filterByArray.push({ - field_name: 'start_from', - field_value: startFromSeconds, - }); - } - - if (startToSeconds) { - filterByArray.push({ - field_name: 'start_to', - field_value: startToSeconds, - }); - } - - if (endFromSeconds) { - filterByArray.push({ - field_name: 'end_from', - field_value: endFromSeconds, - }); - } - - if (endToSeconds) { - filterByArray.push({ - field_name: 'end_to', - field_value: endToSeconds, - }); - } - - reportMetadata.filter_by.forEach((reportFilter: ReportFilter) => { - columnArray.forEach((reportColumn: ReportColumn) => { - if ( - reportColumn.accessor === reportFilter.field_name && - reportColumn.filterable - ) { - filterByArray.push(reportFilter); - } - }); - }); + // // TODO: make a field to set this + // let orderByArray = ['-start_in_seconds', '-id']; + // if (orderBy) { + // orderByArray = orderBy.split(',').filter((n) => n); + // } + // const filterByArray: any = []; + // + // if (processModelSelection) { + // filterByArray.push({ + // field_name: 'process_model_identifier', + // field_value: processModelSelection.id, + // }); + // } + // + // if (processInitiatorSelection) { + // filterByArray.push({ + // field_name: 'process_initiator_username', + // field_value: processInitiatorSelection.username, + // }); + // } + // + // if (processStatusSelection.length > 0) { + // filterByArray.push({ + // field_name: 'process_status', + // field_value: processStatusSelection.join(','), + // operator: 'in', + // }); + // } + // + // if (startFromSeconds) { + // filterByArray.push({ + // field_name: 'start_from', + // field_value: startFromSeconds, + // }); + // } + // + // if (startToSeconds) { + // filterByArray.push({ + // field_name: 'start_to', + // field_value: startToSeconds, + // }); + // } + // + // if (endFromSeconds) { + // filterByArray.push({ + // field_name: 'end_from', + // field_value: endFromSeconds, + // }); + // } + // + // if (endToSeconds) { + // filterByArray.push({ + // field_name: 'end_to', + // field_value: endToSeconds, + // }); + // } + // + // reportMetadata.filter_by.forEach((reportFilter: ReportFilter) => { + // columnArray.forEach((reportColumn: ReportColumn) => { + // if ( + // reportColumn.accessor === reportFilter.field_name && + // reportColumn.filterable + // ) { + // filterByArray.push(reportFilter); + // } + // }); + // }); let path = `/process-instances/reports`; let httpMethod = 'POST'; @@ -156,11 +156,7 @@ export default function ProcessInstanceListSaveAsReport({ httpMethod, postBody: { identifier, - report_metadata: { - columns: columnArray, - order_by: orderByArray, - filter_by: filterByArray, - }, + report_metadata: reportMetadata, }, }); handleSaveFormClose(); diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 85de9ba9..39f7871b 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; // @ts-ignore @@ -37,10 +37,7 @@ import { convertSecondsToFormattedDateString, convertSecondsToFormattedDateTime, convertSecondsToFormattedTimeHoursMinutes, - decodeBase64, - encodeBase64, getPageInfoFromSearchParams, - getProcessModelFullIdentifierFromSearchParams, modifyProcessIdentifierForPathParam, refreshAtInterval, REFRESH_INTERVAL_SECONDS, @@ -119,6 +116,7 @@ export default function ProcessInstanceListTable({ if (variant === 'all') { processInstanceApiSearchPath = '/process-instances'; } + const params = useParams(); const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); @@ -282,9 +280,11 @@ export default function ProcessInstanceListTable({ setProcessInstanceFilters(result.filters); setReportMetadata(result.report.report_metadata); - if (result.report.id) { - setProcessInstanceReportSelection(result.report); - } + // if (processInstanceReport) { + // if (processInstanceReport.id > 0) { + // setProcessInstanceReportSelection(processInstanceReport); + // } + // } if (result.report_hash) { searchParams.set('report_hash', result.report_hash); setSearchParams(searchParams); @@ -301,6 +301,11 @@ export default function ProcessInstanceListTable({ } }; + const getData = useCallback(() => { + console.log('WE DO STUFF'); + stopRefreshing(null); + }, []); + // eslint-disable-next-line sonarjs/cognitive-complexity useEffect(() => { if (!permissionsLoaded) { @@ -318,9 +323,6 @@ export default function ProcessInstanceListTable({ setPagination(result.pagination); setProcessInstanceFilters(result.filters); setReportMetadata(result.report.report_metadata); - if (result.report.id) { - setProcessInstanceReportSelection(result.report); - } } // Useful to stop refreshing if an api call gets an error @@ -334,18 +336,21 @@ export default function ProcessInstanceListTable({ } }; function getProcessInstances( - reportMetadataBody: ReportMetadata | null = null + processInstanceReport: ProcessInstanceReport | null = null ) { if (listHasBeenFiltered) { return; } - let reportMetadataBodyToUse = reportMetadataBody; - if (!reportMetadataBodyToUse) { - reportMetadataBodyToUse = { - columns: [], - filter_by: [], - order_by: [], - }; + let reportMetadataBodyToUse: ReportMetadata = { + columns: [], + filter_by: [], + order_by: [], + }; + if (processInstanceReport) { + reportMetadataBodyToUse = processInstanceReport.report_metadata; + if (processInstanceReport.id > 0) { + setProcessInstanceReportSelection(processInstanceReport); + } } let selectedProcessModelIdentifier = processModelFullIdentifier; @@ -391,22 +396,6 @@ export default function ProcessInstanceListTable({ } ); - // if (searchParams.get('report_id')) { - // queryParamString += `&report_id=${searchParams.get('report_id')}`; - // } else if (reportIdentifier) { - // queryParamString += `&report_identifier=${reportIdentifier}`; - // } - - // if (searchParams.get('report_columns')) { - // const reportColumnsBase64 = searchParams.get('report_columns'); - // if (reportColumnsBase64) { - // const reportColumnsList = JSON.parse( - // decodeBase64(reportColumnsBase64) - // ); - // postBody.columns = reportColumnsList; - // } - // } - // eslint-disable-next-line prefer-const let { page, perPage } = getPageInfoFromSearchParams( searchParams, @@ -427,8 +416,8 @@ export default function ProcessInstanceListTable({ path: `${processInstanceApiSearchPath}?${queryParamString}`, successCallback: setProcessInstancesFromResult, httpMethod: 'POST', - failureCallback: stopRefreshing, - onUnauthorized: stopRefreshing, + failureCallback: getData, + onUnauthorized: getData, postBody: { report_metadata: reportMetadataBodyToUse, }, @@ -438,10 +427,19 @@ export default function ProcessInstanceListTable({ if (listHasBeenFiltered) { return; } - const reportHash = searchParams.get('report_hash'); - if (reportHash) { + const queryParams: string[] = []; + ['report_hash', 'report_id', 'report_identifier'].forEach( + (paramName: string) => { + if (searchParams.get(paramName)) { + queryParams.push(`${paramName}=${searchParams.get(paramName)}`); + } + } + ); + + if (queryParams.length > 0) { + const queryParamString = `?${queryParams.join('&')}`; HttpService.makeCallToBackend({ - path: `/process-instances/report-metadata/${reportHash}`, + path: `/process-instances/report-metadata${queryParamString}`, successCallback: getProcessInstances, }); } else { @@ -697,6 +695,85 @@ export default function ProcessInstanceListTable({ } }; + const getNewReportMetadataBasedOnPageWidgets = () => { + const { + valid, + startFromSeconds, + startToSeconds, + endFromSeconds, + endToSeconds, + } = calculateStartAndEndSeconds(); + + if (!valid) { + return null; + } + + let newReportMetadata = null; + if (reportMetadata) { + newReportMetadata = { ...reportMetadata }; + } + if (!newReportMetadata) { + newReportMetadata = { + columns: [], + filter_by: [], + order_by: [], + }; + } + + addFieldValueToReportMetadata( + newReportMetadata, + 'start_from', + startFromSeconds + ); + addFieldValueToReportMetadata( + newReportMetadata, + 'start_to', + startToSeconds + ); + addFieldValueToReportMetadata( + newReportMetadata, + 'end_from', + endFromSeconds + ); + addFieldValueToReportMetadata(newReportMetadata, 'end_to', endToSeconds); + if (processStatusSelection.length > 0) { + addFieldValueToReportMetadata( + newReportMetadata, + 'process_status', + processStatusSelection.join(',') + ); + } else { + removeFieldFromReportMetadata(newReportMetadata, 'process_status'); + } + + if (processModelSelection) { + addFieldValueToReportMetadata( + newReportMetadata, + 'process_model_identifier', + processModelSelection.id + ); + } else { + removeFieldFromReportMetadata( + newReportMetadata, + 'process_model_identifier' + ); + } + + if (processInitiatorSelection) { + addFieldValueToReportMetadata( + newReportMetadata, + 'process_initiator_username', + processInitiatorSelection.username + ); + } else { + removeFieldFromReportMetadata( + newReportMetadata, + 'process_initiator_username' + ); + } + return newReportMetadata; + }; + const applyFilter = (event: any) => { event.preventDefault(); setProcessInitiatorNotFoundErrorText(''); @@ -707,74 +784,10 @@ export default function ProcessInstanceListTable({ undefined, paginationQueryParamPrefix ); - const { - valid, - startFromSeconds, - startToSeconds, - endFromSeconds, - endToSeconds, - } = calculateStartAndEndSeconds(); - - if (!valid) { - return; - } - - let postBody = null; - if (reportMetadata) { - postBody = { ...reportMetadata }; - } - if (!postBody) { - postBody = { - columns: [], - filter_by: [], - order_by: [], - }; - } - - addFieldValueToReportMetadata(postBody, 'start_from', startFromSeconds); - addFieldValueToReportMetadata(postBody, 'start_to', startToSeconds); - addFieldValueToReportMetadata(postBody, 'end_from', endFromSeconds); - addFieldValueToReportMetadata(postBody, 'end_to', endToSeconds); - if (processStatusSelection.length > 0) { - addFieldValueToReportMetadata( - postBody, - 'process_status', - processStatusSelection.join(',') - ); - } else { - removeFieldFromReportMetadata(postBody, 'process_status'); - } - - if (processModelSelection) { - addFieldValueToReportMetadata( - postBody, - 'process_model_identifier', - processModelSelection.id - ); - } else { - removeFieldFromReportMetadata(postBody, 'process_model_identifier'); - } - - // if (processInstanceReportSelection) { - // addFieldValueToReportMetadata( - // postBody, - // 'report_id', - // processInstanceReportSelection.id.toString() - // ); - // } - - if (processInitiatorSelection) { - addFieldValueToReportMetadata( - postBody, - 'process_initiator_username', - processInitiatorSelection.username - ); - } else { - removeFieldFromReportMetadata(postBody, 'process_initiator_username'); - } + const newReportMetadata = getNewReportMetadataBasedOnPageWidgets(); setListHasBeenFiltered(true); - setReportMetadata(postBody); + setReportMetadata(newReportMetadata); searchParams.set('per_page', perPage.toString()); searchParams.set('page', page.toString()); setSearchParams(searchParams); @@ -783,7 +796,7 @@ export default function ProcessInstanceListTable({ HttpService.makeCallToBackend({ path: `${processInstanceApiSearchPath}?${queryParamString}`, httpMethod: 'POST', - postBody: { report_metadata: postBody }, + postBody: { report_metadata: newReportMetadata }, failureCallback: stopRefreshing, onUnauthorized: stopRefreshing, successCallback: (result: any) => { @@ -907,14 +920,12 @@ export default function ProcessInstanceListTable({ }); }; - // TODO onSuccess reload/select the new report in the report search - const onSaveReportSuccess = (result: any, mode: string) => { - processInstanceReportDidChange( - { - selectedItem: result, - }, - mode - ); + const onSaveReportSuccess = ( + processInstanceReport: ProcessInstanceReport + ) => { + setProcessInstanceReportSelection(processInstanceReport); + searchParams.set('report_id', processInstanceReport.id.toString()); + setSearchParams(searchParams); }; const saveAsReportComponent = () => { @@ -926,7 +937,9 @@ export default function ProcessInstanceListTable({ endToSeconds, } = calculateStartAndEndSeconds(false); - if (!valid || !reportMetadata) { + const newReportMetadata = getNewReportMetadataBasedOnPageWidgets(); + + if (!valid || !reportMetadata || !newReportMetadata) { return null; } return ( @@ -940,7 +953,7 @@ export default function ProcessInstanceListTable({ processInitiatorSelection={processInitiatorSelection} processStatusSelection={processStatusSelection} processInstanceReportSelection={processInstanceReportSelection} - reportMetadata={reportMetadata} + reportMetadata={newReportMetadata} startFromSeconds={startFromSeconds} startToSeconds={startToSeconds} endFromSeconds={endFromSeconds} @@ -1052,15 +1065,15 @@ export default function ProcessInstanceListTable({ { filter_field_value: '', filter_operator: '' } ); if (reportColumn.filterable) { - const reportFilter = getFilterByFromReportMetadata( - reportColumnForEditing.accessor - ); - if (reportFilter) { - reportColumnForEditing.filter_field_value = - reportFilter.field_value || ''; - reportColumnForEditing.filter_operator = - reportFilter.operator || 'equals'; - } + const reportFilter = getFilterByFromReportMetadata( + reportColumnForEditing.accessor + ); + if (reportFilter) { + reportColumnForEditing.filter_field_value = + reportFilter.field_value || ''; + reportColumnForEditing.filter_operator = + reportFilter.operator || 'equals'; + } } return reportColumnForEditing; }; @@ -1622,6 +1635,8 @@ export default function ProcessInstanceListTable({ , ]; diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceReportSearch.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceReportSearch.tsx index 644244d7..1c5ba33d 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceReportSearch.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceReportSearch.tsx @@ -5,7 +5,6 @@ import { FormLabel, // @ts-ignore } from '@carbon/react'; -import { useSearchParams } from 'react-router-dom'; import { truncateString } from '../helpers'; import { ProcessInstanceReport } from '../interfaces'; import HttpService from '../services/HttpService'; @@ -14,32 +13,42 @@ type OwnProps = { onChange: (..._args: any[]) => any; selectedItem?: ProcessInstanceReport | null; titleText?: string; + selectedReportId?: string | null; + handleSetSelectedReportCallback?: Function; }; export default function ProcessInstanceReportSearch({ selectedItem, onChange, + selectedReportId, + handleSetSelectedReportCallback, titleText = 'Process instance perspectives', }: OwnProps) { const [processInstanceReports, setProcessInstanceReports] = useState< ProcessInstanceReport[] | null >(null); - const [searchParams] = useSearchParams(); - const reportId = searchParams.get('report_id'); - useEffect(() => { + const selectedReportIdAsNumber = Number(selectedReportId); + function setProcessInstanceReportsFromResult( result: ProcessInstanceReport[] ) { setProcessInstanceReports(result); + if (selectedReportId && handleSetSelectedReportCallback) { + result.forEach((processInstanceReport: ProcessInstanceReport) => { + if (processInstanceReport.id === selectedReportIdAsNumber) { + handleSetSelectedReportCallback(processInstanceReport); + } + }); + } } HttpService.makeCallToBackend({ path: `/process-instances/reports`, successCallback: setProcessInstanceReportsFromResult, }); - }, [reportId]); + }, [handleSetSelectedReportCallback, selectedReportId]); const reportSelectionString = ( processInstanceReport: ProcessInstanceReport diff --git a/spiffworkflow-frontend/src/routes/AdminRoutes.tsx b/spiffworkflow-frontend/src/routes/AdminRoutes.tsx index d04d50b1..0e9df5d9 100644 --- a/spiffworkflow-frontend/src/routes/AdminRoutes.tsx +++ b/spiffworkflow-frontend/src/routes/AdminRoutes.tsx @@ -8,7 +8,6 @@ import ProcessGroupEdit from './ProcessGroupEdit'; import ProcessModelShow from './ProcessModelShow'; import ProcessModelEditDiagram from './ProcessModelEditDiagram'; import ProcessInstanceList from './ProcessInstanceList'; -import ProcessInstanceReportShow from './ProcessInstanceReportShow'; import ProcessModelNew from './ProcessModelNew'; import ProcessModelEdit from './ProcessModelEdit'; import ProcessInstanceShow from './ProcessInstanceShow'; @@ -88,10 +87,6 @@ export default function AdminRoutes() { path="process-instances/reports" element={} /> - } - /> } diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceReportShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceReportShow.tsx deleted file mode 100644 index 38622691..00000000 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceReportShow.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useParams, useSearchParams } from 'react-router-dom'; - -// @ts-ignore -import { Button, Table } from '@carbon/react'; -import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; - -import PaginationForTable from '../components/PaginationForTable'; -import HttpService from '../services/HttpService'; -import { getPageInfoFromSearchParams } from '../helpers'; - -const PER_PAGE_FOR_PROCESS_INSTANCE_REPORT = 500; - -export default function ProcessInstanceReport() { - const params = useParams(); - const [searchParams] = useSearchParams(); - - const [processInstances, setProcessInstances] = useState([]); - const [reportMetadata, setReportMetadata] = useState({}); - const [pagination, setPagination] = useState(null); - - useEffect(() => { - const processResult = (result: any) => { - const processInstancesFromApi = result.results; - setProcessInstances(processInstancesFromApi); - setReportMetadata(result.report_metadata); - setPagination(result.pagination); - }; - - function getProcessInstances() { - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_PROCESS_INSTANCE_REPORT - ); - let query = `?page=${page}&per_page=${perPage}`; - searchParams.forEach((value, key) => { - if (key !== 'page' && key !== 'per_page') { - query += `&${key}=${value}`; - } - }); - HttpService.makeCallToBackend({ - path: `/process-instances/reports/${params.report_identifier}${query}`, - successCallback: processResult, - }); - } - - getProcessInstances(); - }, [searchParams, params]); - - const buildTable = () => { - const headers = (reportMetadata as any).columns.map((column: any) => { - return {(column as any).Header}; - }); - - const rows = processInstances.map((row) => { - const currentRow = (reportMetadata as any).columns.map((column: any) => { - return {(row as any)[column.accessor]}; - }); - return {currentRow}; - }); - return ( - - - {headers} - - {rows} -
- ); - }; - - if (pagination) { - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_PROCESS_INSTANCE_REPORT - ); - return ( -
- -

Process Instance Perspective: {params.report_identifier}

- - -
- ); - } - return null; -} From a4fd81cb9af27c5dcc6cefbefa0053040cbee933 Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 27 Apr 2023 17:33:25 -0400 Subject: [PATCH 07/18] remove a lot of unused code and fixed linting issues w/ burnettk --- .../bin/import_tickets_for_script_task.py | 108 --- .../spiffworkflow_backend/models/json_data.py | 6 +- .../models/process_instance_report.py | 120 +-- .../routes/process_instances_controller.py | 108 +-- .../routes/tasks_controller.py | 2 +- .../process_instance_report_service.py | 596 +++++--------- .../services/task_service.py | 4 +- .../integration/test_process_api.py | 45 +- .../unit/test_process_instance_processor.py | 4 +- .../test_process_instance_report_service.py | 758 +----------------- spiffworkflow-frontend/.eslintrc.js | 1 + .../ProcessInstanceListSaveAsReport.tsx | 95 +-- .../components/ProcessInstanceListTable.tsx | 189 +---- 13 files changed, 301 insertions(+), 1735 deletions(-) delete mode 100644 spiffworkflow-backend/bin/import_tickets_for_script_task.py diff --git a/spiffworkflow-backend/bin/import_tickets_for_script_task.py b/spiffworkflow-backend/bin/import_tickets_for_script_task.py deleted file mode 100644 index 9550699c..00000000 --- a/spiffworkflow-backend/bin/import_tickets_for_script_task.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Import tickets, for use in script task.""" - - -def main(): - """Use main to avoid global namespace.""" - import csv - - from spiffworkflow_backend.models.db import db - - from spiffworkflow_backend.models.process_instance import ProcessInstanceModel - from spiffworkflow_backend.models.user import UserModel - from spiffworkflow_backend.services.process_instance_processor import ( - ProcessInstanceProcessor, - ) - from spiffworkflow_backend.services.process_instance_service import ( - ProcessInstanceService, - ) - from spiffworkflow_backend.models.process_instance_report import ( - ProcessInstanceReportModel, - ) - - process_model_identifier_ticket = "ticket" - db.session.query(ProcessInstanceModel).filter( - ProcessInstanceModel.process_model_identifier == process_model_identifier_ticket - ).delete() - db.session.commit() - - """Print process instance count.""" - process_instances = ProcessInstanceModel.query.filter_by( - process_model_identifier=process_model_identifier_ticket - ).all() - process_instance_count = len(process_instances) - print(f"process_instance_count: {process_instance_count}") - - columns_to_data_key_mappings = { - "Month": "month", - "MS": "milestone", - "Done?": "done", - "#": "notion_id", - "ID": "req_id", - "Dev Days": "dev_days", - "Feature": "feature", - "Feature description": "feature_description", - "Priority": "priority", - } - columns_to_header_index_mappings = {} - - user = UserModel.query.first() - - with open("tests/files/tickets.csv") as infile: - reader = csv.reader(infile, delimiter=",") - - # first row is garbage - next(reader) - - header = next(reader) - for column_name in columns_to_data_key_mappings: - columns_to_header_index_mappings[column_name] = header.index(column_name) - id_index = header.index("ID") - priority_index = header.index("Priority") - month_index = header.index("Month") - print(f"header: {header}") - for row in reader: - ticket_identifier = row[id_index] - priority = row[priority_index] - month = row[month_index] - print(f"ticket_identifier: {ticket_identifier}") - print(f"priority: {priority}") - # if there is no month, who cares about it. - if month: - process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier( - process_model_identifier=process_model_identifier_ticket, - user=user, - process_group_identifier="sartography-admin", - ) - processor = ProcessInstanceProcessor(process_instance) - - processor.do_engine_steps() - # processor.save() - - for ( - column_name, - desired_data_key, - ) in columns_to_data_key_mappings.items(): - appropriate_index = columns_to_header_index_mappings[column_name] - print(f"appropriate_index: {appropriate_index}") - processor.bpmn_process_instance.data[desired_data_key] = row[appropriate_index] - - # you at least need a month, or else this row in the csv is considered garbage - month_value = processor.bpmn_process_instance.data["month"] - if month_value == "" or month_value is None: - db.session.delete(process_instance) - db.session.commit() - continue - - processor.save() - - process_instance_data = processor.get_data() - print(f"process_instance_data: {process_instance_data}") - - ProcessInstanceReportModel.add_fixtures() - print("added report fixtures") - - -main() - -# to avoid serialization issues -del main diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/json_data.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/json_data.py index 5c094b86..be4eb8d7 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/json_data.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/json_data.py @@ -1,8 +1,10 @@ from __future__ import annotations + import json from hashlib import sha256 -from flask import current_app from typing import TypedDict + +from flask import current_app from sqlalchemy.dialects.mysql import insert as mysql_insert from sqlalchemy.dialects.postgresql import insert as postgres_insert @@ -76,6 +78,6 @@ class JsonDataModel(SpiffworkflowBaseDBModel): @classmethod def create_and_insert_json_data_from_dict(cls, data: dict) -> str: json_data_hash = sha256(json.dumps(data, sort_keys=True).encode("utf8")).hexdigest() - cls.insert_or_update_json_data_dict({'hash': json_data_hash, 'data': data}) + cls.insert_or_update_json_data_dict({"hash": json_data_hash, "data": data}) db.session.commit() return json_data_hash diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py index 25497b1c..74ee1a48 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py @@ -1,21 +1,20 @@ """Process_instance.""" from __future__ import annotations -from spiffworkflow_backend.models.json_data import JsonDataModel # noqa: F401 +import typing from dataclasses import dataclass from typing import Any from typing import cast +from typing import NotRequired from typing import Optional from typing import TypedDict from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship -from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( - ProcessEntityNotFoundError, -) from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel +from spiffworkflow_backend.models.json_data import JsonDataModel # noqa: F401 from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.process_instance_processor import ( @@ -25,13 +24,14 @@ from spiffworkflow_backend.services.process_instance_processor import ( class FilterValue(TypedDict): field_name: str - field_value: str - operator: str + field_value: str | int | bool + operator: NotRequired[str] class ReportMetadataColumn(TypedDict): Header: str accessor: str + filterable: NotRequired[bool] class ReportMetadata(TypedDict): @@ -83,7 +83,7 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): id: int = db.Column(db.Integer, primary_key=True) identifier: str = db.Column(db.String(50), nullable=False, index=True) - report_metadata: dict = db.Column(db.JSON) + report_metadata: ReportMetadata = db.Column(db.JSON) created_by_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True) # type: ignore created_by = relationship("UserModel") created_at_in_seconds = db.Column(db.Integer) @@ -99,49 +99,6 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): """Default_order_by.""" return ["-start_in_seconds", "-id"] - @classmethod - def add_fixtures(cls) -> None: - """Add_fixtures.""" - try: - # process_model = ProcessModelService.get_process_model( - # process_model_id="sartography-admin/ticket" - # ) - user = UserModel.query.first() - columns = [ - {"Header": "id", "accessor": "id"}, - {"Header": "month", "accessor": "month"}, - {"Header": "milestone", "accessor": "milestone"}, - {"Header": "req_id", "accessor": "req_id"}, - {"Header": "feature", "accessor": "feature"}, - {"Header": "dev_days", "accessor": "dev_days"}, - {"Header": "priority", "accessor": "priority"}, - ] - json = {"order": "month asc", "columns": columns} - - cls.create_report( - identifier="standard", - user=user, - report_metadata=json, - ) - cls.create_report( - identifier="for-month", - user=user, - report_metadata=cls.ticket_for_month_report(), - ) - cls.create_report( - identifier="for-month-3", - user=user, - report_metadata=cls.ticket_for_month_3_report(), - ) - cls.create_report( - identifier="hot-report", - user=user, - report_metadata=cls.process_model_with_form_report_fixture(), - ) - - except ProcessEntityNotFoundError: - print("Did not find process models so not adding report fixtures for them") - @classmethod def create_report( cls, @@ -160,7 +117,8 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): f"Process instance report with identifier already exists: {identifier}" ) - json_data_hash = JsonDataModel.create_and_insert_json_data_from_dict(report_metadata) + report_metadata_dict = typing.cast(typing.Dict[str, Any], report_metadata) + json_data_hash = JsonDataModel.create_and_insert_json_data_from_dict(report_metadata_dict) process_instance_report = cls( identifier=identifier, @@ -173,69 +131,11 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): return process_instance_report # type: ignore - @classmethod - def ticket_for_month_report(cls) -> dict: - """Ticket_for_month_report.""" - return { - "columns": [ - {"Header": "id", "accessor": "id"}, - {"Header": "month", "accessor": "month"}, - {"Header": "milestone", "accessor": "milestone"}, - {"Header": "req_id", "accessor": "req_id"}, - {"Header": "feature", "accessor": "feature"}, - {"Header": "priority", "accessor": "priority"}, - ], - "order": "month asc", - "filter_by": [ - { - "field_name": "month", - "operator": "equals", - "field_value": "{{month}}", - } - ], - } - - @classmethod - def ticket_for_month_3_report(cls) -> dict: - """Ticket_for_month_report.""" - return { - "columns": [ - {"Header": "id", "accessor": "id"}, - {"Header": "month", "accessor": "month"}, - {"Header": "milestone", "accessor": "milestone"}, - {"Header": "req_id", "accessor": "req_id"}, - {"Header": "feature", "accessor": "feature"}, - {"Header": "dev_days", "accessor": "dev_days"}, - {"Header": "priority", "accessor": "priority"}, - ], - "order": "month asc", - "filter_by": [{"field_name": "month", "operator": "equals", "field_value": "3"}], - } - - @classmethod - def process_model_with_form_report_fixture(cls) -> dict: - """Process_model_with_form_report_fixture.""" - return { - "columns": [ - {"Header": "id", "accessor": "id"}, - { - "Header": "system_generated_number", - "accessor": "system_generated_number", - }, - { - "Header": "user_generated_number", - "accessor": "user_generated_number", - }, - {"Header": "product", "accessor": "product"}, - ], - "order": "-id", - } - @classmethod def create_with_attributes( cls, identifier: str, - report_metadata: dict, + report_metadata: ReportMetadata, user: UserModel, ) -> ProcessInstanceReportModel: """Create_with_attributes.""" 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 a9471c9a..dd249372 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -1,7 +1,4 @@ """APIs for dealing with process groups, process models, and process instances.""" -import base64 -from hashlib import sha256 -from spiffworkflow_backend.models.json_data import JsonDataModel # noqa: F401 import json from typing import Any from typing import Dict @@ -12,7 +9,6 @@ from flask import current_app from flask import g from flask import jsonify from flask import make_response -from flask import request from flask.wrappers import Response from sqlalchemy import and_ from sqlalchemy import or_ @@ -26,6 +22,7 @@ from spiffworkflow_backend.models.bpmn_process_definition import ( from spiffworkflow_backend.models.db import db 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 # noqa: F401 from spiffworkflow_backend.models.process_instance import ProcessInstanceApiSchema from spiffworkflow_backend.models.process_instance import ( ProcessInstanceCannotBeDeletedError, @@ -70,9 +67,6 @@ from spiffworkflow_backend.services.process_instance_queue_service import ( from spiffworkflow_backend.services.process_instance_queue_service import ( ProcessInstanceQueueService, ) -# from spiffworkflow_backend.services.process_instance_report_service import ( -# ProcessInstanceReportFilter, -# ) from spiffworkflow_backend.services.process_instance_report_service import ( ProcessInstanceReportService, ) @@ -83,6 +77,10 @@ from spiffworkflow_backend.services.process_model_service import ProcessModelSer from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.task_service import TaskService +# from spiffworkflow_backend.services.process_instance_report_service import ( +# ProcessInstanceReportFilter, +# ) + def process_instance_create( modified_process_model_identifier: str, @@ -232,38 +230,13 @@ def process_instance_list_for_me( process_model_identifier: Optional[str] = None, page: int = 1, per_page: int = 100, - # start_from: Optional[int] = None, - # start_to: Optional[int] = None, - # end_from: Optional[int] = None, - # end_to: Optional[int] = None, - # process_status: Optional[str] = None, - # user_filter: Optional[bool] = False, - # report_identifier: Optional[str] = None, - # report_id: Optional[int] = None, - # user_group_identifier: Optional[str] = None, - # process_initiator_username: Optional[str] = None, - # report_columns: Optional[str] = None, - # report_filter_by: Optional[str] = None, ) -> flask.wrappers.Response: """Process_instance_list_for_me.""" return process_instance_list( process_model_identifier=process_model_identifier, page=page, per_page=per_page, - # start_from=start_from, - # start_to=start_to, - # end_from=end_from, - # end_to=end_to, - # process_status=process_status, - # user_filter=user_filter, - # report_identifier=report_identifier, - # report_id=report_id, - # user_group_identifier=user_group_identifier, - # with_relation_to_me=True, - # report_columns=report_columns, - # report_filter_by=report_filter_by, - # process_initiator_username=process_initiator_username, - body=body + body=body, ) @@ -272,71 +245,16 @@ def process_instance_list( process_model_identifier: Optional[str] = None, page: int = 1, per_page: int = 100, - # start_from: Optional[int] = None, - # start_to: Optional[int] = None, - # end_from: Optional[int] = None, - # end_to: Optional[int] = None, - # process_status: Optional[str] = None, - # with_relation_to_me: Optional[bool] = None, - # user_filter: Optional[bool] = False, - # report_identifier: Optional[str] = None, - # report_id: Optional[int] = None, - # user_group_identifier: Optional[str] = None, - # process_initiator_username: Optional[str] = None, - # report_columns: Optional[str] = None, - # report_filter_by: Optional[str] = None, ) -> flask.wrappers.Response: - # process_instance_report = ProcessInstanceReportService.report_with_identifier(g.user, body['report_id'], body['report_identifier']) - process_instance_report = ProcessInstanceReportService.report_with_identifier(g.user) - - # report_column_list = None - # if report_columns: - # report_column_list = json.loads(base64.b64decode(report_columns)) - # report_filter_by_list = None - # if report_filter_by: - # report_filter_by_list = json.loads(base64.b64decode(report_filter_by)) - - # if user_filter: - # report_filter = ProcessInstanceReportFilter( - # 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, - # # with_relation_to_me=with_relation_to_me, - # # process_status=process_status.split(",") if process_status else None, - # # process_initiator_username=process_initiator_username, - # # report_column_list=report_column_list, - # # report_filter_by_list=report_filter_by_list, - # ) - # else: - # report_filter = ProcessInstanceReportService.filter_from_metadata_with_overrides( - # process_instance_report=process_instance_report, - # 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, - # # with_relation_to_me=with_relation_to_me, - # # process_initiator_username=process_initiator_username, - # # report_column_list=report_column_list, - # # report_filter_by_list=report_filter_by_list, - # ) - response_json = ProcessInstanceReportService.run_process_instance_report( - # report_filter=report_filter, - report_metadata=body['report_metadata'], - process_instance_report=process_instance_report, + report_metadata=body["report_metadata"], page=page, per_page=per_page, user=g.user, ) - json_data_hash = JsonDataModel.create_and_insert_json_data_from_dict(body['report_metadata']) - response_json['report_hash'] = json_data_hash + json_data_hash = JsonDataModel.create_and_insert_json_data_from_dict(body["report_metadata"]) + response_json["report_hash"] = json_data_hash db.session.commit() return make_response(jsonify(response_json), 200) @@ -347,11 +265,13 @@ def process_instance_report_show( report_id: Optional[int] = None, report_identifier: Optional[str] = None, ) -> flask.wrappers.Response: - if report_hash is None and report_id is None and report_identifier is None: raise ApiError( error_code="report_key_missing", - message="A report key is needed to lookup a report. Either choose a report_hash, report_id, or report_identifier.", + message=( + "A report key is needed to lookup a report. Either choose a report_hash, report_id, or" + " report_identifier." + ), ) response_result = {} if report_hash is not None: @@ -454,7 +374,6 @@ def process_instance_report_list(page: int = 1, per_page: int = 100) -> flask.wr def process_instance_report_create(body: Dict[str, Any]) -> flask.wrappers.Response: - """Process_instance_report_create.""" process_instance_report = ProcessInstanceReportModel.create_report( identifier=body["identifier"], user=g.user, @@ -541,6 +460,7 @@ def process_instance_report_delete( # return Response(json.dumps(result_dict), status=200, mimetype="application/json") # + def process_instance_task_list_without_task_data_for_me( modified_process_model_identifier: str, process_instance_id: int, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index b62cd1c1..bf2d8bc9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -1,6 +1,5 @@ """APIs for dealing with process groups, process models, and process instances.""" import json -from spiffworkflow_backend.models.json_data import JsonDataModel # noqa: F401 import os import uuid from sys import exc_info @@ -37,6 +36,7 @@ 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 # noqa: F401 from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus 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 ab180fee..a95cec34 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 @@ -1,7 +1,7 @@ """Process_instance_report_service.""" import re -from dataclasses import dataclass -from typing import Any, Generator, Iterable +from typing import Any +from typing import Generator from typing import Optional from typing import Type @@ -23,11 +23,10 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance_metadata import ( ProcessInstanceMetadataModel, ) -from spiffworkflow_backend.models.process_instance_report import ( - FilterValue, - ProcessInstanceReportModel, - ReportMetadata, -) +from spiffworkflow_backend.models.process_instance_report import FilterValue +from spiffworkflow_backend.models.process_instance_report import ProcessInstanceReportModel +from spiffworkflow_backend.models.process_instance_report import ReportMetadata +from spiffworkflow_backend.models.process_instance_report import ReportMetadataColumn from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel from spiffworkflow_backend.services.process_model_service import ProcessModelService @@ -37,196 +36,144 @@ class ProcessInstanceReportNotFoundError(Exception): """ProcessInstanceReportNotFoundError.""" -# @dataclass -# class ProcessInstanceReportFilter: -# """ProcessInstanceReportFilter.""" -# -# process_model_identifier: Optional[str] = None -# user_group_identifier: Optional[str] = None -# start_from: Optional[int] = None -# start_to: Optional[int] = None -# end_from: Optional[int] = None -# end_to: Optional[int] = None -# process_status: Optional[list[str]] = None -# initiated_by_me: Optional[bool] = None -# has_terminal_status: Optional[bool] = None -# has_active_status: Optional[bool] = None -# with_tasks_completed_by_me: Optional[bool] = None -# with_tasks_i_can_complete: 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 -# report_column_list: Optional[list] = None -# report_filter_by_list: Optional[list] = None -# oldest_open_human_task_fields: Optional[list] = None -# -# def to_dict(self) -> dict[str, str]: -# """To_dict.""" -# d = {} -# -# if self.process_model_identifier is not None: -# d["process_model_identifier"] = self.process_model_identifier -# if self.user_group_identifier is not None: -# d["user_group_identifier"] = self.user_group_identifier -# if self.start_from is not None: -# d["start_from"] = str(self.start_from) -# if self.start_to is not None: -# d["start_to"] = str(self.start_to) -# if self.end_from is not None: -# d["end_from"] = str(self.end_from) -# if self.end_to is not None: -# 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.has_terminal_status is not None: -# d["has_terminal_status"] = str(self.has_terminal_status).lower() -# if self.has_active_status is not None: -# d["has_active_status"] = str(self.has_active_status).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_i_can_complete is not None: -# d["with_tasks_i_can_complete"] = str(self.with_tasks_i_can_complete).lower() -# if self.with_tasks_assigned_to_my_group is not None: -# d["with_tasks_assigned_to_my_group"] = str(self.with_tasks_assigned_to_my_group).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) -# if self.report_column_list is not None: -# d["report_column_list"] = str(self.report_column_list) -# if self.report_filter_by_list is not None: -# d["report_filter_by_list"] = str(self.report_filter_by_list) -# if self.oldest_open_human_task_fields is not None: -# d["oldest_open_human_task_fields"] = str(self.oldest_open_human_task_fields) -# -# return d - - class ProcessInstanceReportService: """ProcessInstanceReportService.""" @classmethod - def system_metadata_map(cls, metadata_key: str) -> Optional[dict[str, Any]]: + def system_metadata_map(cls, metadata_key: str) -> Optional[ReportMetadata]: """System_metadata_map.""" # TODO replace with system reports that are loaded on launch (or similar) - temp_system_metadata_map = { - "default": { - "columns": cls.builtin_column_options(), - "filter_by": [], - "order_by": ["-start_in_seconds", "-id"], - }, - "system_report_completed_instances_initiated_by_me": { - "columns": [ - {"Header": "id", "accessor": "id"}, - { - "Header": "process_model_display_name", - "accessor": "process_model_display_name", - }, - {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, - {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, - {"Header": "status", "accessor": "status"}, - ], - "filter_by": [ - {"field_name": "initiated_by_me", "field_value": "true"}, - {"field_name": "has_terminal_status", "field_value": "true"}, - ], - "order_by": ["-start_in_seconds", "-id"], - }, - "system_report_completed_instances_with_tasks_completed_by_me": { - "columns": cls.builtin_column_options(), - "filter_by": [ - {"field_name": "with_tasks_completed_by_me", "field_value": "true"}, - {"field_name": "has_terminal_status", "field_value": "true"}, - ], - "order_by": ["-start_in_seconds", "-id"], - }, - "system_report_completed_instances_with_tasks_completed_by_my_groups": { - "columns": cls.builtin_column_options(), - "filter_by": [ - { - "field_name": "with_tasks_assigned_to_my_group", - "field_value": "true", - }, - {"field_name": "has_terminal_status", "field_value": "true"}, - ], - "order_by": ["-start_in_seconds", "-id"], - }, - "system_report_in_progress_instances_initiated_by_me": { - "columns": [ - {"Header": "id", "accessor": "id"}, - { - "Header": "process_model_display_name", - "accessor": "process_model_display_name", - }, - {"Header": "Task", "accessor": "task_title"}, - {"Header": "Waiting For", "accessor": "waiting_for"}, - {"Header": "Started", "accessor": "start_in_seconds"}, - {"Header": "Last Updated", "accessor": "updated_at_in_seconds"}, - {"Header": "status", "accessor": "status"}, - ], - "filter_by": [ - {"field_name": "initiated_by_me", "field_value": "true"}, - {"field_name": "has_terminal_status", "field_value": "false"}, - { - "field_name": "oldest_open_human_task_fields", - "field_value": ( - "task_id,task_title,task_name,potential_owner_usernames,assigned_user_group_identifier" - ), - }, - ], - "order_by": ["-start_in_seconds", "-id"], - }, - "system_report_in_progress_instances_with_tasks_for_me": { - "columns": [ - {"Header": "id", "accessor": "id"}, - { - "Header": "process_model_display_name", - "accessor": "process_model_display_name", - }, - {"Header": "Task", "accessor": "task_title"}, - {"Header": "Started By", "accessor": "process_initiator_username"}, - {"Header": "Started", "accessor": "start_in_seconds"}, - {"Header": "Last Updated", "accessor": "updated_at_in_seconds"}, - ], - "filter_by": [ - {"field_name": "with_tasks_i_can_complete", "field_value": "true"}, - {"field_name": "has_active_status", "field_value": "true"}, - { - "field_name": "oldest_open_human_task_fields", - "field_value": "task_id,task_title,task_name", - }, - ], - "order_by": ["-start_in_seconds", "-id"], - }, - "system_report_in_progress_instances_with_tasks_for_my_group": { - "columns": [ - {"Header": "id", "accessor": "id"}, - { - "Header": "process_model_display_name", - "accessor": "process_model_display_name", - }, - {"Header": "Task", "accessor": "task_title"}, - {"Header": "Started By", "accessor": "process_initiator_username"}, - {"Header": "Started", "accessor": "start_in_seconds"}, - {"Header": "Last Updated", "accessor": "updated_at_in_seconds"}, - ], - "filter_by": [ - { - "field_name": "with_tasks_assigned_to_my_group", - "field_value": "true", - }, - {"field_name": "has_active_status", "field_value": "true"}, - { - "field_name": "oldest_open_human_task_fields", - "field_value": "task_id,task_title,task_name", - }, - ], - "order_by": ["-start_in_seconds", "-id"], - }, + default: ReportMetadata = { + "columns": cls.builtin_column_options(), + "filter_by": [], + "order_by": ["-start_in_seconds", "-id"], + } + system_report_completed_instances_initiated_by_me: ReportMetadata = { + "columns": [ + {"Header": "id", "accessor": "id"}, + { + "Header": "process_model_display_name", + "accessor": "process_model_display_name", + }, + {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, + {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, + {"Header": "status", "accessor": "status"}, + ], + "filter_by": [ + {"field_name": "initiated_by_me", "field_value": True}, + {"field_name": "has_terminal_status", "field_value": True}, + ], + "order_by": ["-start_in_seconds", "-id"], + } + system_report_completed_instances_with_tasks_completed_by_me: ReportMetadata = { + "columns": cls.builtin_column_options(), + "filter_by": [ + {"field_name": "with_tasks_completed_by_me", "field_value": True}, + {"field_name": "has_terminal_status", "field_value": True}, + ], + "order_by": ["-start_in_seconds", "-id"], + } + system_report_completed_instances_with_tasks_completed_by_my_groups: ReportMetadata = { + "columns": cls.builtin_column_options(), + "filter_by": [ + { + "field_name": "with_tasks_assigned_to_my_group", + "field_value": True, + }, + {"field_name": "has_terminal_status", "field_value": True}, + ], + "order_by": ["-start_in_seconds", "-id"], + } + system_report_in_progress_instances_initiated_by_me: ReportMetadata = { + "columns": [ + {"Header": "id", "accessor": "id"}, + { + "Header": "process_model_display_name", + "accessor": "process_model_display_name", + }, + {"Header": "Task", "accessor": "task_title"}, + {"Header": "Waiting For", "accessor": "waiting_for"}, + {"Header": "Started", "accessor": "start_in_seconds"}, + {"Header": "Last Updated", "accessor": "updated_at_in_seconds"}, + {"Header": "status", "accessor": "status"}, + ], + "filter_by": [ + {"field_name": "initiated_by_me", "field_value": True}, + {"field_name": "has_terminal_status", "field_value": False}, + { + "field_name": "oldest_open_human_task_fields", + "field_value": ( + "task_id,task_title,task_name,potential_owner_usernames,assigned_user_group_identifier" + ), + }, + ], + "order_by": ["-start_in_seconds", "-id"], + } + system_report_in_progress_instances_with_tasks_for_me: ReportMetadata = { + "columns": [ + {"Header": "id", "accessor": "id"}, + { + "Header": "process_model_display_name", + "accessor": "process_model_display_name", + }, + {"Header": "Task", "accessor": "task_title"}, + {"Header": "Started By", "accessor": "process_initiator_username"}, + {"Header": "Started", "accessor": "start_in_seconds"}, + {"Header": "Last Updated", "accessor": "updated_at_in_seconds"}, + ], + "filter_by": [ + {"field_name": "with_tasks_i_can_complete", "field_value": True}, + {"field_name": "has_active_status", "field_value": True}, + { + "field_name": "oldest_open_human_task_fields", + "field_value": "task_id,task_title,task_name", + }, + ], + "order_by": ["-start_in_seconds", "-id"], + } + system_report_in_progress_instances_with_tasks_for_my_group: ReportMetadata = { + "columns": [ + {"Header": "id", "accessor": "id"}, + { + "Header": "process_model_display_name", + "accessor": "process_model_display_name", + }, + {"Header": "Task", "accessor": "task_title"}, + {"Header": "Started By", "accessor": "process_initiator_username"}, + {"Header": "Started", "accessor": "start_in_seconds"}, + {"Header": "Last Updated", "accessor": "updated_at_in_seconds"}, + ], + "filter_by": [ + { + "field_name": "with_tasks_assigned_to_my_group", + "field_value": True, + }, + {"field_name": "has_active_status", "field_value": True}, + { + "field_name": "oldest_open_human_task_fields", + "field_value": "task_id,task_title,task_name", + }, + ], + "order_by": ["-start_in_seconds", "-id"], } + temp_system_metadata_map = { + "default": default, + "system_report_completed_instances_initiated_by_me": system_report_completed_instances_initiated_by_me, + "system_report_completed_instances_with_tasks_completed_by_me": ( + system_report_completed_instances_with_tasks_completed_by_me + ), + "system_report_completed_instances_with_tasks_completed_by_my_groups": ( + system_report_completed_instances_with_tasks_completed_by_my_groups + ), + "system_report_in_progress_instances_initiated_by_me": system_report_in_progress_instances_initiated_by_me, + "system_report_in_progress_instances_with_tasks_for_me": ( + system_report_in_progress_instances_with_tasks_for_me + ), + "system_report_in_progress_instances_with_tasks_for_my_group": ( + system_report_in_progress_instances_with_tasks_for_my_group + ), + } if metadata_key not in temp_system_metadata_map: return None return temp_system_metadata_map[metadata_key] @@ -268,160 +215,20 @@ class ProcessInstanceReportService: return process_instance_report # type: ignore - # @classmethod - # def filter_by_to_dict(cls, process_instance_report: ProcessInstanceReportModel) -> dict[str, str]: - # """Filter_by_to_dict.""" - # metadata = process_instance_report.report_metadata - # filter_by = metadata.get("filter_by", []) - # filters = {d["field_name"]: d["field_value"] for d in filter_by if "field_name" in d and "field_value" in d} - # return filters - # - # @classmethod - # def filter_from_metadata(cls, process_instance_report: ProcessInstanceReportModel) -> ProcessInstanceReportFilter: - # """Filter_from_metadata.""" - # filters = cls.filter_by_to_dict(process_instance_report) - # - # def bool_value(key: str) -> Optional[bool]: - # """Bool_value.""" - # if key not in filters: - # return None - # # bool returns True if not an empty string so check explicitly for false - # if filters[key] in ["false", "False"]: - # return False - # return bool(filters[key]) - # - # def int_value(key: str) -> Optional[int]: - # """Int_value.""" - # return int(filters[key]) if key in filters else None - # - # def list_value(key: str) -> Optional[list[str]]: - # return filters[key].split(",") if key in filters else None - # - # process_model_identifier = filters.get("process_model_identifier") - # user_group_identifier = filters.get("user_group_identifier") - # start_from = int_value("start_from") - # start_to = int_value("start_to") - # 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") - # has_terminal_status = bool_value("has_terminal_status") - # has_active_status = bool_value("has_active_status") - # with_tasks_completed_by_me = bool_value("with_tasks_completed_by_me") - # with_tasks_i_can_complete = bool_value("with_tasks_i_can_complete") - # 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_column_list = list_value("report_column_list") - # report_filter_by_list = list_value("report_filter_by_list") - # oldest_open_human_task_fields = list_value("oldest_open_human_task_fields") - # - # report_filter = ProcessInstanceReportFilter( - # 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, - # has_active_status=has_active_status, - # with_tasks_completed_by_me=with_tasks_completed_by_me, - # with_tasks_i_can_complete=with_tasks_i_can_complete, - # 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, - # report_column_list=report_column_list, - # report_filter_by_list=report_filter_by_list, - # oldest_open_human_task_fields=oldest_open_human_task_fields, - # ) - # - # return report_filter - # - # @classmethod - # def filter_from_metadata_with_overrides( - # cls, - # process_instance_report: ProcessInstanceReportModel, - # process_model_identifier: Optional[str] = None, - # user_group_identifier: Optional[str] = None, - # start_from: Optional[int] = None, - # start_to: Optional[int] = None, - # end_from: Optional[int] = None, - # end_to: Optional[int] = None, - # process_status: Optional[str] = None, - # initiated_by_me: Optional[bool] = None, - # has_terminal_status: Optional[bool] = None, - # has_active_status: Optional[bool] = None, - # with_tasks_completed_by_me: Optional[bool] = None, - # with_tasks_i_can_complete: 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, - # report_column_list: Optional[list] = None, - # report_filter_by_list: Optional[list] = None, - # oldest_open_human_task_fields: Optional[list] = None, - # ) -> ProcessInstanceReportFilter: - # """Filter_from_metadata_with_overrides.""" - # report_filter = cls.filter_from_metadata(process_instance_report) - # - # if process_model_identifier is not None: - # report_filter.process_model_identifier = process_model_identifier - # if user_group_identifier is not None: - # report_filter.user_group_identifier = user_group_identifier - # if start_from is not None: - # report_filter.start_from = start_from - # if start_to is not None: - # report_filter.start_to = start_to - # if end_from is not None: - # report_filter.end_from = end_from - # if end_to is not None: - # 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 has_terminal_status is not None: - # report_filter.has_terminal_status = has_terminal_status - # if has_active_status is not None: - # report_filter.has_active_status = has_active_status - # if with_tasks_completed_by_me is not None: - # report_filter.with_tasks_completed_by_me = with_tasks_completed_by_me - # if with_tasks_i_can_complete is not None: - # report_filter.with_tasks_i_can_complete = with_tasks_i_can_complete - # if process_initiator_username is not None: - # report_filter.process_initiator_username = process_initiator_username - # if report_column_list is not None: - # report_filter.report_column_list = report_column_list - # if report_filter_by_list is not None: - # report_filter.report_filter_by_list = report_filter_by_list - # if oldest_open_human_task_fields is not None: - # report_filter.oldest_open_human_task_fields = oldest_open_human_task_fields - # if with_tasks_assigned_to_my_group is not None: - # report_filter.with_tasks_assigned_to_my_group = with_tasks_assigned_to_my_group - # if with_relation_to_me is not None: - # report_filter.with_relation_to_me = with_relation_to_me - # - # return report_filter - @classmethod def add_metadata_columns_to_process_instance( cls, process_instance_sqlalchemy_rows: list[sqlalchemy.engine.row.Row], # type: ignore - metadata_columns: list[dict], + metadata_columns: list[ReportMetadataColumn], ) -> list[dict]: """Add_metadata_columns_to_process_instance.""" results = [] - non_metadata_columns = cls.non_metadata_columns() + cls.non_metadata_columns() for process_instance_row in process_instance_sqlalchemy_rows: process_instance_mapping = process_instance_row._mapping process_instance_dict = process_instance_row[0].serialized - # import pdb; pdb.set_trace() for metadata_column in metadata_columns: - # if metadata_column["accessor"] not in non_metadata_columns: - # if metadata_column["accessor"] not in cls.process_instance_stock_columns(): if metadata_column["accessor"] not in process_instance_dict: - # import pdb; pdb.set_trace() process_instance_dict[metadata_column["accessor"]] = process_instance_mapping[ metadata_column["accessor"] ] @@ -488,12 +295,12 @@ class ProcessInstanceReportService: @classmethod def non_metadata_columns(cls) -> list[str]: - return cls.process_instance_stock_columns() + ['process_initiator_username'] + return cls.process_instance_stock_columns() + ["process_initiator_username"] @classmethod - def builtin_column_options(cls) -> list[dict]: + def builtin_column_options(cls) -> list[ReportMetadataColumn]: """Builtin_column_options.""" - return [ + return_value: list[ReportMetadataColumn] = [ {"Header": "Id", "accessor": "id", "filterable": False}, { "Header": "Process", @@ -509,12 +316,13 @@ class ProcessInstanceReportService: }, {"Header": "Status", "accessor": "status", "filterable": False}, ] + return return_value @classmethod def get_filter_value(cls, filters: list[FilterValue], filter_key: str) -> Any: for filter in filters: - if filter['field_name'] == filter_key and filter['field_value'] is not None: - return filter['field_value'] + if filter["field_name"] == filter_key and filter["field_value"] is not None: + return filter["field_value"] @classmethod def check_filter_value(cls, filters: list[FilterValue], filter_key: str) -> Generator: @@ -525,9 +333,7 @@ class ProcessInstanceReportService: @classmethod def run_process_instance_report( cls, - # report_filter: ProcessInstanceReportFilter, report_metadata: ReportMetadata, - process_instance_report: ProcessInstanceReportModel, user: UserModel, page: int = 1, per_page: int = 100, @@ -535,9 +341,9 @@ class ProcessInstanceReportService: process_instance_query = ProcessInstanceModel.query # Always join that hot user table for good performance at serialization time. process_instance_query = process_instance_query.options(selectinload(ProcessInstanceModel.process_initiator)) - filters = report_metadata['filter_by'] + filters = report_metadata["filter_by"] - for value in cls.check_filter_value(filters, 'process_model_identifier'): + for value in cls.check_filter_value(filters, "process_model_identifier"): process_model = ProcessModelService.get_process_model( f"{value}", ) @@ -553,32 +359,24 @@ class ProcessInstanceReportService: ) ) - for value in cls.check_filter_value(filters, 'start_from'): + for value in cls.check_filter_value(filters, "start_from"): + process_instance_query = process_instance_query.filter(ProcessInstanceModel.start_in_seconds >= value) + for value in cls.check_filter_value(filters, "start_to"): + process_instance_query = process_instance_query.filter(ProcessInstanceModel.start_in_seconds <= value) + for value in cls.check_filter_value(filters, "end_from"): + process_instance_query = process_instance_query.filter(ProcessInstanceModel.end_in_seconds >= value) + for value in cls.check_filter_value(filters, "end_to"): + process_instance_query = process_instance_query.filter(ProcessInstanceModel.end_in_seconds <= value) + for value in cls.check_filter_value(filters, "process_status"): process_instance_query = process_instance_query.filter( - ProcessInstanceModel.start_in_seconds >= value - ) - for value in cls.check_filter_value(filters, 'start_to'): - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.start_in_seconds <= value - ) - for value in cls.check_filter_value(filters, 'end_from'): - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.end_in_seconds >= value - ) - for value in cls.check_filter_value(filters, 'end_to'): - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.end_in_seconds <= value - ) - for value in cls.check_filter_value(filters, 'process_status'): - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(value.split(',')) # type: ignore + ProcessInstanceModel.status.in_(value.split(",")) # type: ignore ) - for value in cls.check_filter_value(filters, 'initiated_by_me'): + for value in cls.check_filter_value(filters, "initiated_by_me"): if value is True: process_instance_query = process_instance_query.filter_by(process_initiator=user) - for value in cls.check_filter_value(filters, 'has_terminal_status'): + for value in cls.check_filter_value(filters, "has_terminal_status"): if value is True: process_instance_query = process_instance_query.filter( ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore @@ -587,25 +385,25 @@ class ProcessInstanceReportService: process_instance_query = process_instance_query.filter( ProcessInstanceModel.status.not_in(ProcessInstanceModel.terminal_statuses()) # type: ignore ) - for value in cls.check_filter_value(filters, 'has_active_status'): + for value in cls.check_filter_value(filters, "has_active_status"): if value is True: process_instance_query = process_instance_query.filter( ProcessInstanceModel.status.in_(ProcessInstanceModel.active_statuses()) # type: ignore ) - for value in cls.check_filter_value(filters, 'process_initiator_username'): + for value in cls.check_filter_value(filters, "process_initiator_username"): initiator = UserModel.query.filter_by(username=value).first() process_initiator_id = -1 if initiator: process_initiator_id = initiator.id process_instance_query = process_instance_query.filter_by(process_initiator_id=process_initiator_id) - with_tasks_completed_by_me = cls.get_filter_value(filters, 'with_tasks_completed_by_me') - with_tasks_assigned_to_my_group = cls.get_filter_value(filters, 'with_tasks_assigned_to_my_group') - with_tasks_i_can_complete = cls.get_filter_value(filters, 'with_tasks_i_can_complete') - with_relation_to_me = cls.get_filter_value(filters, 'with_relation_to_me') - has_active_status = cls.get_filter_value(filters, 'has_active_status') - user_group_identifier = cls.get_filter_value(filters, 'user_group_identifier') + with_tasks_completed_by_me = cls.get_filter_value(filters, "with_tasks_completed_by_me") + with_tasks_assigned_to_my_group = cls.get_filter_value(filters, "with_tasks_assigned_to_my_group") + with_tasks_i_can_complete = cls.get_filter_value(filters, "with_tasks_i_can_complete") + with_relation_to_me = cls.get_filter_value(filters, "with_relation_to_me") + has_active_status = cls.get_filter_value(filters, "has_active_status") + user_group_identifier = cls.get_filter_value(filters, "user_group_identifier") if ( not with_tasks_completed_by_me and not with_tasks_assigned_to_my_group @@ -676,27 +474,19 @@ class ProcessInstanceReportService: process_instance_query = process_instance_query.filter(UserGroupAssignmentModel.user_id == user.id) instance_metadata_aliases = {} - # print(f"report_metadata['columns']: {report_metadata['columns']}") - # import pdb; pdb.set_trace() - if report_metadata['columns'] and len(report_metadata['columns']) > 0: - process_instance_report.report_metadata["columns"] = report_metadata['columns'] - if report_metadata['filter_by'] and len(report_metadata['filter_by']) > 0: - process_instance_report.report_metadata["filter_by"] = report_metadata['filter_by'] + if report_metadata["columns"] is None or len(report_metadata["columns"]) < 1: + report_metadata["columns"] = cls.builtin_column_options() - for column in process_instance_report.report_metadata["columns"]: + for column in report_metadata["columns"]: if column["accessor"] in cls.non_metadata_columns(): continue instance_metadata_alias = aliased(ProcessInstanceMetadataModel) instance_metadata_aliases[column["accessor"]] = instance_metadata_alias filter_for_column = None - if "filter_by" in process_instance_report.report_metadata: + if "filter_by" in report_metadata: filter_for_column = next( - ( - f - for f in process_instance_report.report_metadata["filter_by"] - if f["field_name"] == column["accessor"] - ), + (f for f in report_metadata["filter_by"] if f["field_name"] == column["accessor"]), None, ) isouter = True @@ -711,41 +501,37 @@ class ProcessInstanceReportService: instance_metadata_alias, and_(*conditions), isouter=isouter ).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"])) - # order_by_query_array = [] - # order_by_array = process_instance_report.report_metadata["order_by"] - # if len(order_by_array) < 1: - # order_by_array = ProcessInstanceReportModel.default_order_by() - # for order_by_option in order_by_array: - # attribute = re.sub("^-", "", order_by_option) - # if attribute in cls.process_instance_stock_columns(): - # if order_by_option.startswith("-"): - # order_by_query_array.append(getattr(ProcessInstanceModel, attribute).desc()) - # else: - # order_by_query_array.append(getattr(ProcessInstanceModel, attribute).asc()) - # elif attribute in instance_metadata_aliases: - # if order_by_option.startswith("-"): - # order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).desc()) - # else: - # order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).asc()) + order_by_query_array = [] + order_by_array = report_metadata["order_by"] + if len(order_by_array) < 1: + order_by_array = ProcessInstanceReportModel.default_order_by() + for order_by_option in order_by_array: + attribute = re.sub("^-", "", order_by_option) + if attribute in cls.process_instance_stock_columns(): + if order_by_option.startswith("-"): + order_by_query_array.append(getattr(ProcessInstanceModel, attribute).desc()) + else: + order_by_query_array.append(getattr(ProcessInstanceModel, attribute).asc()) + elif attribute in instance_metadata_aliases: + if order_by_option.startswith("-"): + order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).desc()) + else: + order_by_query_array.append(func.max(instance_metadata_aliases[attribute].value).asc()) process_instances = ( process_instance_query.group_by(ProcessInstanceModel.id) .add_columns(ProcessInstanceModel.id) - # .order_by(*order_by_query_array) + .order_by(*order_by_query_array) .paginate(page=page, per_page=per_page, error_out=False) ) - results = cls.add_metadata_columns_to_process_instance( - process_instances.items, process_instance_report.report_metadata["columns"] - ) - # import pdb; pdb.set_trace() + results = cls.add_metadata_columns_to_process_instance(process_instances.items, report_metadata["columns"]) - for value in cls.check_filter_value(filters, 'oldest_open_human_task_fields'): + for value in cls.check_filter_value(filters, "oldest_open_human_task_fields"): results = cls.add_human_task_fields(results, value) response_json = { - "report": process_instance_report, + "report_metadata": report_metadata, "results": results, "filters": filters, - "hash": "HEY", "pagination": { "count": len(results), "total": process_instances.total, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py index ec088dd3..4b22605c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py @@ -8,7 +8,6 @@ from typing import TypedDict from typing import Union from uuid import UUID -from flask import current_app from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflow # type: ignore from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer from SpiffWorkflow.exceptions import WorkflowException # type: ignore @@ -20,7 +19,8 @@ from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel from spiffworkflow_backend.models.bpmn_process import BpmnProcessNotFoundError from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel from spiffworkflow_backend.models.db import db -from spiffworkflow_backend.models.json_data import JsonDataDict, JsonDataModel # noqa: F401 +from spiffworkflow_backend.models.json_data import JsonDataDict +from spiffworkflow_backend.models.json_data import JsonDataModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType 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 fa49cac0..8fec7d26 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -25,9 +25,8 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.models.process_instance_metadata import ( ProcessInstanceMetadataModel, ) -from spiffworkflow_backend.models.process_instance_report import ( - ProcessInstanceReportModel, -) +from spiffworkflow_backend.models.process_instance_report import ProcessInstanceReportModel +from spiffworkflow_backend.models.process_instance_report import ReportMetadata from spiffworkflow_backend.models.process_model import NotificationType from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema from spiffworkflow_backend.models.spec_reference import SpecReferenceCache @@ -1975,7 +1974,7 @@ class TestProcessApi(BaseTest): self.logged_in_headers(with_super_admin_user) report_identifier = "testreport" - report_metadata = {"order_by": ["month"]} + report_metadata: ReportMetadata = {"order_by": ["month"], "filter_by": [], "columns": []} ProcessInstanceReportModel.create_with_attributes( identifier=report_identifier, report_metadata=report_metadata, @@ -3078,12 +3077,12 @@ class TestProcessApi(BaseTest): ).all() assert len(process_instance_metadata) == 3 - report_metadata = { + report_metadata: ReportMetadata = { "columns": [ - {"Header": "ID", "accessor": "id"}, - {"Header": "Status", "accessor": "status"}, - {"Header": "Key One", "accessor": "key1"}, - {"Header": "Key Two", "accessor": "key2"}, + {"Header": "ID", "accessor": "id", "filterable": False}, + {"Header": "Status", "accessor": "status", "filterable": False}, + {"Header": "Key One", "accessor": "key1", "filterable": False}, + {"Header": "Key Two", "accessor": "key2", "filterable": False}, ], "order_by": ["status"], "filter_by": [], @@ -3130,11 +3129,11 @@ class TestProcessApi(BaseTest): 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 = { + dne_report_metadata: ReportMetadata = { "columns": [ - {"Header": "ID", "accessor": "id"}, - {"Header": "Status", "accessor": "status"}, - {"Header": "Process Initiator", "accessor": "username"}, + {"Header": "ID", "accessor": "id", "filterable": False}, + {"Header": "Status", "accessor": "status", "filterable": False}, + {"Header": "Process Initiator", "accessor": "username", "filterable": False}, ], "order_by": ["status"], "filter_by": [ @@ -3146,11 +3145,11 @@ class TestProcessApi(BaseTest): ], } - user_one_report_metadata = { + user_one_report_metadata: ReportMetadata = { "columns": [ - {"Header": "ID", "accessor": "id"}, - {"Header": "Status", "accessor": "status"}, - {"Header": "Process Initiator", "accessor": "username"}, + {"Header": "ID", "accessor": "id", "filterable": False}, + {"Header": "Status", "accessor": "status", "filterable": False}, + {"Header": "Process Initiator", "accessor": "username", "filterable": False}, ], "order_by": ["status"], "filter_by": [ @@ -3308,12 +3307,13 @@ class TestProcessApi(BaseTest): processor.do_engine_steps(save=True) assert process_instance_two.status == "complete" - report_metadata = { + report_metadata: ReportMetadata = { "columns": [ - {"Header": "id", "accessor": "id"}, - {"Header": "Time", "accessor": "time_ns"}, + {"Header": "id", "accessor": "id", "filterable": True}, + {"Header": "Time", "accessor": "time_ns", "filterable": True}, ], "order_by": ["time_ns"], + "filter_by": [], } report_one = ProcessInstanceReportModel.create_with_attributes( identifier="report_one", @@ -3333,10 +3333,11 @@ class TestProcessApi(BaseTest): report_metadata = { "columns": [ - {"Header": "id", "accessor": "id"}, - {"Header": "Time", "accessor": "time_ns"}, + {"Header": "id", "accessor": "id", "filterable": True}, + {"Header": "Time", "accessor": "time_ns", "filterable": True}, ], "order_by": ["-time_ns"], + "filter_by": [], } report_two = ProcessInstanceReportModel.create_with_attributes( identifier="report_two", diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py index 57e1cbfc..e89a959a 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py @@ -362,7 +362,9 @@ class TestProcessInstanceProcessor(BaseTest): human_task_one = process_instance.active_human_tasks[0] spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id)) ProcessInstanceService.complete_form_task(processor, spiff_manual_task, {}, initiator_user, human_task_one) - import pdb; pdb.set_trace() + import pdb + + pdb.set_trace() assert ( len(process_instance.active_human_tasks) == 1 ), "expected 1 active human tasks after 2nd one is completed, as we have looped back around." diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py index 3542e7dd..e7e2ad61 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report_service.py @@ -1,6 +1,4 @@ """Test_process_instance_report_service.""" -from typing import Optional - from flask import Flask from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest @@ -9,738 +7,13 @@ from tests.spiffworkflow_backend.helpers.test_data import load_test_spec 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.process_instance_report import ( - ProcessInstanceReportModel, -) -from spiffworkflow_backend.models.user import UserModel -from spiffworkflow_backend.services.process_instance_report_service import ( - ProcessInstanceReportFilter, -) from spiffworkflow_backend.services.process_instance_report_service import ( ProcessInstanceReportService, ) from spiffworkflow_backend.services.user_service import UserService -class TestProcessInstanceReportFilter(BaseTest): - """TestProcessInstanceReportFilter.""" - - def test_empty_filter_to_dict( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - d = ProcessInstanceReportFilter().to_dict() - - assert d == {} - - def test_string_value_filter_to_dict( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - d = ProcessInstanceReportFilter(process_model_identifier="bob").to_dict() - - assert d == {"process_model_identifier": "bob"} - - def test_int_value_filter_to_dict( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - d = ProcessInstanceReportFilter( - start_from=1, - start_to=2, - end_from=3, - end_to=4, - ).to_dict() - - assert d == { - "start_from": "1", - "start_to": "2", - "end_from": "3", - "end_to": "4", - } - - def test_list_single_value_filter_to_dict( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - d = ProcessInstanceReportFilter(process_status=["bob"]).to_dict() - - assert d == {"process_status": "bob"} - - def test_list_multiple_value_filter_to_dict( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - d = ProcessInstanceReportFilter(process_status=["joe", "bob", "sue"]).to_dict() - - assert d == {"process_status": "joe,bob,sue"} - - class TestProcessInstanceReportService(BaseTest): - """TestProcessInstanceReportService.""" - - def _filter_from_metadata(self, report_metadata: dict) -> ProcessInstanceReportFilter: - """Docstring.""" - report = ProcessInstanceReportModel( - identifier="test", - created_by_id=1, - report_metadata=report_metadata, - ) - return ProcessInstanceReportService.filter_from_metadata(report) - - def _filter_from_metadata_with_overrides( - self, - report_metadata: dict, - process_model_identifier: Optional[str] = None, - start_from: Optional[int] = None, - start_to: Optional[int] = None, - end_from: Optional[int] = None, - end_to: Optional[int] = None, - process_status: Optional[str] = None, - ) -> ProcessInstanceReportFilter: - """Docstring.""" - report = ProcessInstanceReportModel( - identifier="test", - created_by_id=1, - report_metadata=report_metadata, - ) - return ProcessInstanceReportService.filter_from_metadata_with_overrides( - process_instance_report=report, - process_model_identifier=process_model_identifier, - start_from=start_from, - start_to=start_to, - end_from=end_from, - end_to=end_to, - process_status=process_status, - ) - - def _filter_by_dict_from_metadata(self, report_metadata: dict) -> dict[str, str]: - """Docstring.""" - report = ProcessInstanceReportModel( - identifier="test", - created_by_id=1, - report_metadata=report_metadata, - ) - return ProcessInstanceReportService.filter_by_to_dict(report) - - def test_filter_by_to_dict_no_filter_by( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - filters = self._filter_by_dict_from_metadata( - { - "columns": [], - } - ) - - assert filters == {} - - def test_filter_by_to_dict_empty_filter_by( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - filters = self._filter_by_dict_from_metadata( - { - "columns": [], - "filter_by": [], - } - ) - - assert filters == {} - - def test_filter_by_to_dict_single_filter_by( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - filters = self._filter_by_dict_from_metadata( - { - "columns": [], - "filter_by": [{"field_name": "end_to", "field_value": "1234"}], - } - ) - - assert filters == {"end_to": "1234"} - - def test_filter_by_to_dict_mulitple_filter_by( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - filters = self._filter_by_dict_from_metadata( - { - "columns": [], - "filter_by": [ - {"field_name": "end_to", "field_value": "1234"}, - {"field_name": "end_from", "field_value": "4321"}, - ], - } - ) - - assert filters == {"end_to": "1234", "end_from": "4321"} - - def test_report_with_no_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata( - { - "columns": [], - } - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status is None - - def test_report_with_empty_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata( - { - "columns": [], - "filter_by": [], - } - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status is None - - def test_report_with_unknown_filter_field_name( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata( - { - "columns": [], - "filter_by": [{"field_name": "bob", "field_value": "joe"}], - } - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status is None - - def test_report_with_unknown_filter_keys( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata( - { - "columns": [], - "filter_by": [{"_name": "bob", "_value": "joe"}], - } - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status is None - - def test_report_with_process_model_identifier_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata( - { - "columns": [], - "filter_by": [{"field_name": "process_model_identifier", "field_value": "bob"}], - } - ) - - assert report_filter.process_model_identifier == "bob" - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status is None - - def test_report_with_start_from_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata( - { - "columns": [], - "filter_by": [{"field_name": "start_from", "field_value": "1234"}], - } - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from == 1234 - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status is None - - def test_report_with_start_to_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata( - { - "columns": [], - "filter_by": [{"field_name": "start_to", "field_value": "1234"}], - } - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to == 1234 - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status is None - - def test_report_with_end_from_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata( - { - "columns": [], - "filter_by": [{"field_name": "end_from", "field_value": "1234"}], - } - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from == 1234 - assert report_filter.end_to is None - assert report_filter.process_status is None - - def test_report_with_end_to_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata( - { - "columns": [], - "filter_by": [{"field_name": "end_to", "field_value": "1234"}], - } - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to == 1234 - assert report_filter.process_status is None - - def test_report_with_single_startus_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata( - { - "columns": [], - "filter_by": [{"field_name": "process_status", "field_value": "ready"}], - } - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status == ["ready"] - - def test_report_with_multiple_startus_filters( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata( - { - "columns": [], - "filter_by": [ - { - "field_name": "process_status", - "field_value": "ready,completed,other", - } - ], - } - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status == ["ready", "completed", "other"] - - def test_report_with_multiple_filters( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata( - { - "columns": [], - "filter_by": [ - {"field_name": "start_from", "field_value": "44"}, - {"field_name": "end_from", "field_value": "55"}, - {"field_name": "process_status", "field_value": "ready"}, - ], - } - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from == 44 - assert report_filter.start_to is None - assert report_filter.end_from == 55 - assert report_filter.end_to is None - assert report_filter.process_status == ["ready"] - - def test_report_no_override_with_no_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata_with_overrides( - { - "columns": [], - }, - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status is None - - def test_report_override_with_no_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata_with_overrides( - { - "columns": [], - }, - end_to=54321, - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to == 54321 - assert report_filter.process_status is None - - def test_report_override_process_model_identifier_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata_with_overrides( - { - "columns": [], - "filter_by": [{"field_name": "process_model_identifier", "field_value": "bob"}], - }, - process_model_identifier="joe", - ) - - assert report_filter.process_model_identifier == "joe" - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status is None - - def test_report_override_start_from_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata_with_overrides( - { - "columns": [], - "filter_by": [{"field_name": "start_from", "field_value": "123"}], - }, - start_from=321, - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from == 321 - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status is None - - def test_report_override_start_to_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata_with_overrides( - { - "columns": [], - "filter_by": [{"field_name": "start_to", "field_value": "123"}], - }, - start_to=321, - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to == 321 - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status is None - - def test_report_override_end_from_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata_with_overrides( - { - "columns": [], - "filter_by": [{"field_name": "end_from", "field_value": "123"}], - }, - end_from=321, - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from == 321 - assert report_filter.end_to is None - assert report_filter.process_status is None - - def test_report_override_end_to_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata_with_overrides( - { - "columns": [], - "filter_by": [{"field_name": "end_to", "field_value": "123"}], - }, - end_to=321, - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to == 321 - assert report_filter.process_status is None - - def test_report_override_process_status_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata_with_overrides( - { - "columns": [], - "filter_by": [{"field_name": "process_status", "field_value": "joe,bob"}], - }, - process_status="sue", - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status == ["sue"] - - def test_report_override_mulitple_process_status_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata_with_overrides( - { - "columns": [], - "filter_by": [{"field_name": "process_status", "field_value": "sue"}], - }, - process_status="joe,bob", - ) - - assert report_filter.process_model_identifier is None - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status == ["joe", "bob"] - - def test_report_override_does_not_override_other_filters( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata_with_overrides( - { - "columns": [], - "filter_by": [ - {"field_name": "process_model_identifier", "field_value": "sue"}, - {"field_name": "process_status", "field_value": "sue"}, - ], - }, - process_status="joe,bob", - ) - - assert report_filter.process_model_identifier == "sue" - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status == ["joe", "bob"] - - def test_report_override_of_none_does_not_override_filter( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Docstring.""" - report_filter = self._filter_from_metadata_with_overrides( - { - "columns": [], - "filter_by": [ - {"field_name": "process_model_identifier", "field_value": "sue"}, - {"field_name": "process_status", "field_value": "sue"}, - ], - }, - process_status=None, - ) - - assert report_filter.process_model_identifier == "sue" - assert report_filter.start_from is None - assert report_filter.start_to is None - assert report_filter.end_from is None - assert report_filter.end_to is None - assert report_filter.process_status == ["sue"] - def test_can_filter_by_completed_instances_initiated_by_me( self, app: Flask, @@ -768,13 +41,8 @@ class TestProcessInstanceReportService(BaseTest): user=user_one, report_identifier="system_report_completed_instances_initiated_by_me", ) - report_filter = ProcessInstanceReportService.filter_from_metadata_with_overrides( - process_instance_report=process_instance_report, - process_model_identifier=process_model.id, - ) response_json = ProcessInstanceReportService.run_process_instance_report( - report_filter=report_filter, - process_instance_report=process_instance_report, + report_metadata=process_instance_report.report_metadata, user=user_one, ) @@ -850,13 +118,8 @@ class TestProcessInstanceReportService(BaseTest): user=user_one, report_identifier="system_report_completed_instances_with_tasks_completed_by_me", ) - report_filter = ProcessInstanceReportService.filter_from_metadata_with_overrides( - process_instance_report=process_instance_report, - process_model_identifier=process_model.id, - ) response_json = ProcessInstanceReportService.run_process_instance_report( - report_filter=report_filter, - process_instance_report=process_instance_report, + report_metadata=process_instance_report.report_metadata, user=user_one, ) @@ -936,13 +199,8 @@ class TestProcessInstanceReportService(BaseTest): user=user_one, report_identifier="system_report_completed_instances_with_tasks_completed_by_my_groups", ) - report_filter = ProcessInstanceReportService.filter_from_metadata_with_overrides( - process_instance_report=process_instance_report, - process_model_identifier=process_model.id, - ) response_json = ProcessInstanceReportService.run_process_instance_report( - report_filter=report_filter, - process_instance_report=process_instance_report, + report_metadata=process_instance_report.report_metadata, user=user_one, ) @@ -1026,14 +284,12 @@ class TestProcessInstanceReportService(BaseTest): UserService.add_user_to_human_tasks_if_appropriate(user_one) process_instance_report = ProcessInstanceReportService.report_with_identifier(user=user_one) - report_filter = ProcessInstanceReportService.filter_from_metadata_with_overrides( - process_instance_report=process_instance_report, - process_model_identifier=process_model.id, - with_relation_to_me=True, + report_metadata = process_instance_report.report_metadata + report_metadata["filter_by"].append( + {"field_name": "with_relation_to_me", "field_value": True, "operator": "equals"} ) response_json = ProcessInstanceReportService.run_process_instance_report( - report_filter=report_filter, - process_instance_report=process_instance_report, + report_metadata=report_metadata, user=user_one, ) diff --git a/spiffworkflow-frontend/.eslintrc.js b/spiffworkflow-frontend/.eslintrc.js index 5ed20900..75ad6cbc 100644 --- a/spiffworkflow-frontend/.eslintrc.js +++ b/spiffworkflow-frontend/.eslintrc.js @@ -27,6 +27,7 @@ module.exports = { rules: { // according to https://github.com/typescript-eslint/typescript-eslint/issues/2621, You should turn off the eslint core rule and turn on the typescript-eslint rule // but not sure which of the above "extends" statements is maybe bringing in eslint core + 'max-len': ['error', { code: 200, ignoreUrls: true }], 'no-shadow': 'off', '@typescript-eslint/no-shadow': ['error'], 'jest/expect-expect': 'off', diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx index 78186d97..b196c2ee 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx @@ -6,27 +6,11 @@ import { Modal, // @ts-ignore } from '@carbon/react'; -import { - ReportFilter, - ProcessInstanceReport, - ProcessModel, - ReportColumn, - ReportMetadata, - User, -} from '../interfaces'; +import { ProcessInstanceReport, ReportMetadata } from '../interfaces'; import HttpService from '../services/HttpService'; type OwnProps = { onSuccess: (..._args: any[]) => any; - columnArray: ReportColumn[]; - orderBy: string; - processModelSelection: ProcessModel | null; - processInitiatorSelection: User | null; - processStatusSelection: string[]; - startFromSeconds: string | null; - startToSeconds: string | null; - endFromSeconds: string | null; - endToSeconds: string | null; buttonText?: string; buttonClassName?: string; processInstanceReportSelection?: ProcessInstanceReport | null; @@ -35,16 +19,7 @@ type OwnProps = { export default function ProcessInstanceListSaveAsReport({ onSuccess, - columnArray, - orderBy, - processModelSelection, - processInitiatorSelection, processInstanceReportSelection, - processStatusSelection, - startFromSeconds, - startToSeconds, - endFromSeconds, - endToSeconds, buttonClassName, buttonText = 'Save as Perspective', reportMetadata, @@ -75,74 +50,6 @@ export default function ProcessInstanceListSaveAsReport({ const addProcessInstanceReport = (event: any) => { event.preventDefault(); - // // TODO: make a field to set this - // let orderByArray = ['-start_in_seconds', '-id']; - // if (orderBy) { - // orderByArray = orderBy.split(',').filter((n) => n); - // } - // const filterByArray: any = []; - // - // if (processModelSelection) { - // filterByArray.push({ - // field_name: 'process_model_identifier', - // field_value: processModelSelection.id, - // }); - // } - // - // if (processInitiatorSelection) { - // filterByArray.push({ - // field_name: 'process_initiator_username', - // field_value: processInitiatorSelection.username, - // }); - // } - // - // if (processStatusSelection.length > 0) { - // filterByArray.push({ - // field_name: 'process_status', - // field_value: processStatusSelection.join(','), - // operator: 'in', - // }); - // } - // - // if (startFromSeconds) { - // filterByArray.push({ - // field_name: 'start_from', - // field_value: startFromSeconds, - // }); - // } - // - // if (startToSeconds) { - // filterByArray.push({ - // field_name: 'start_to', - // field_value: startToSeconds, - // }); - // } - // - // if (endFromSeconds) { - // filterByArray.push({ - // field_name: 'end_from', - // field_value: endFromSeconds, - // }); - // } - // - // if (endToSeconds) { - // filterByArray.push({ - // field_name: 'end_to', - // field_value: endToSeconds, - // }); - // } - // - // reportMetadata.filter_by.forEach((reportFilter: ReportFilter) => { - // columnArray.forEach((reportColumn: ReportColumn) => { - // if ( - // reportColumn.accessor === reportFilter.field_name && - // reportColumn.filterable - // ) { - // filterByArray.push(reportFilter); - // } - // }); - // }); - let path = `/process-instances/reports`; let httpMethod = 'POST'; if (isEditMode() && processInstanceReportSelection) { diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 39f7871b..7284eaf6 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -134,7 +134,6 @@ export default function ProcessInstanceListTable({ const [processInstances, setProcessInstances] = useState([]); const [reportMetadata, setReportMetadata] = useState(); const [pagination, setPagination] = useState(null); - const [processInstanceFilters, setProcessInstanceFilters] = useState({}); const oneHourInSeconds = 3600; const oneMonthInSeconds = oneHourInSeconds * 24 * 30; @@ -197,11 +196,10 @@ export default function ProcessInstanceListTable({ const [processInstanceInitiatorOptions, setProcessInstanceInitiatorOptions] = useState([]); - const [processInitiatorSelection, setProcessInitiatorSelection] = - useState(null); - const [processInitiatorText, setProcessInitiatorText] = useState< + const [processInitiatorSelection, setProcessInitiatorSelection] = useState< string | null >(null); + const [ processInitiatorNotFoundErrorText, setProcessInitiatorNotFoundErrorText, @@ -232,10 +230,12 @@ export default function ProcessInstanceListTable({ inputText: string ) => { if (lastRequestedInitatorSearchTerm.current === result.username_prefix) { - setProcessInstanceInitiatorOptions(result.users); + setProcessInstanceInitiatorOptions( + result.users.map((user: User) => user.username) + ); result.users.forEach((user: User) => { if (user.username === inputText) { - setProcessInitiatorSelection(user); + setProcessInitiatorSelection(user.username); } }); } @@ -272,69 +272,46 @@ export default function ProcessInstanceListTable({ }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - function setProcessInstancesFromResult(result: any) { + + const setProcessInstancesFromResult = useCallback((result: any) => { setRequiresRefilter(false); const processInstancesFromApi = result.results; setProcessInstances(processInstancesFromApi); setPagination(result.pagination); - setProcessInstanceFilters(result.filters); - setReportMetadata(result.report.report_metadata); - // if (processInstanceReport) { - // if (processInstanceReport.id > 0) { - // setProcessInstanceReportSelection(processInstanceReport); - // } - // } + setReportMetadata(result.report_metadata); + }, []); + + const setProcessInstancesFromApplyFilter = (result: any) => { + setProcessInstancesFromResult(result); if (result.report_hash) { searchParams.set('report_hash', result.report_hash); setSearchParams(searchParams); } - } + }; + // Useful to stop refreshing if an api call gets an error + // since those errors can make the page unusable in any way const clearRefreshRef = useRef(null); - const stopRefreshing = (error: any) => { + const stopRefreshing = useCallback((error: any) => { if (clearRefreshRef.current) { clearRefreshRef.current(); } if (error) { console.error(error); } - }; - - const getData = useCallback(() => { - console.log('WE DO STUFF'); - stopRefreshing(null); }, []); // eslint-disable-next-line sonarjs/cognitive-complexity useEffect(() => { if (!permissionsLoaded) { - return; + return undefined; } // we apparently cannot use a state set in a useEffect from within that same useEffect // so use a variable instead let processModelSelectionItemsForUseEffect: ProcessModel[] = []; - function setProcessInstancesFromResult(result: any) { - setRequiresRefilter(false); - const processInstancesFromApi = result.results; - setProcessInstances(processInstancesFromApi); - setPagination(result.pagination); - setProcessInstanceFilters(result.filters); - setReportMetadata(result.report.report_metadata); - } - - // Useful to stop refreshing if an api call gets an error - // since those errors can make the page unusable in any way - const stopRefreshing = (error: any) => { - if (clearRefreshRef.current) { - clearRefreshRef.current(); - } - if (error) { - console.error(error); - } - }; function getProcessInstances( processInstanceReport: ProcessInstanceReport | null = null ) { @@ -362,7 +339,7 @@ export default function ProcessInstanceListTable({ ); setShowFilterOptions(true); } else if (reportFilter.field_name === 'process_initiator_username') { - searchForProcessInitiator(reportFilter.field_value || ''); + setProcessInitiatorSelection(reportFilter.field_value || ''); setShowFilterOptions(true); } else if (reportFilter.field_name === 'process_model_identifier') { selectedProcessModelIdentifier = @@ -416,8 +393,8 @@ export default function ProcessInstanceListTable({ path: `${processInstanceApiSearchPath}?${queryParamString}`, successCallback: setProcessInstancesFromResult, httpMethod: 'POST', - failureCallback: getData, - onUnauthorized: getData, + failureCallback: stopRefreshing, + onUnauthorized: stopRefreshing, postBody: { report_metadata: reportMetadataBodyToUse, }, @@ -502,62 +479,11 @@ export default function ProcessInstanceListTable({ additionalParams, processInstanceApiSearchPath, permissionsLoaded, + listHasBeenFiltered, + setProcessInstancesFromResult, + stopRefreshing, ]); - // // This sets the filter data using the saved reports returned from the initial instance_list query. - // // This could probably be merged into the main useEffect but it works here now. - // useEffect(() => { - // const filters = processInstanceFilters as any; - // // Object.keys(dateParametersToAlwaysFilterBy).forEach((paramName: string) => { - // // const dateFunctionToCall = dateParametersToAlwaysFilterBy[paramName][0]; - // // const timeFunctionToCall = dateParametersToAlwaysFilterBy[paramName][1]; - // // const paramValue = filters[paramName]; - // // dateFunctionToCall(''); - // // timeFunctionToCall(''); - // // if (paramValue) { - // // const dateString = convertSecondsToFormattedDateString( - // // paramValue as any - // // ); - // // dateFunctionToCall(dateString); - // // const timeString = convertSecondsToFormattedTimeHoursMinutes( - // // paramValue as any - // // ); - // // timeFunctionToCall(timeString); - // // setShowFilterOptions(true); - // // } - // // }); - // - // // setProcessModelSelection(null); - // // processModelAvailableItems.forEach((item: any) => { - // // if (item.id === filters.process_model_identifier) { - // // setProcessModelSelection(item); - // // } - // // }); - // - // 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) => { - // const regex = new RegExp(`\\b${processStatusOption}\\b`); - // if (filters.process_status.match(regex)) { - // processStatusSelectedArray.push(processStatusOption); - // } - // }); - // setShowFilterOptions(true); - // } - // // setProcessStatusSelection(processStatusSelectedArray); - // }, [ - // processInstanceFilters, - // dateParametersToAlwaysFilterBy, - // parametersToGetFromSearchParams, - // processModelAvailableItems, - // ]); - const processInstanceReportSaveTag = () => { if (processInstanceReportJustSaved) { let titleOperation = 'Updated'; @@ -656,20 +582,6 @@ export default function ProcessInstanceListTable({ return []; }; - // const reportFilterBy = () => { - // if (reportMetadata) { - // return reportMetadata.filter_by; - // } - // return null - // }; - - const navigateToNewReport = (queryParamString: string) => { - removeError(); - setProcessInstanceReportJustSaved(null); - setProcessInstanceFilters({}); - navigate(`${processInstanceListPathPrefix}?${queryParamString}`); - }; - const removeFieldFromReportMetadata = ( postBody: ReportMetadata, fieldName: string @@ -677,21 +589,21 @@ export default function ProcessInstanceListTable({ const filtersToKeep = postBody.filter_by.filter( (rf: ReportFilter) => rf.field_name !== fieldName ); + // eslint-disable-next-line no-param-reassign postBody.filter_by = filtersToKeep; }; - const addFieldValueToReportMetadata = ( + const insertOrUpdateFieldInReportMetadata = ( postBody: ReportMetadata, fieldName: string, fieldValue: string ) => { + removeFieldFromReportMetadata(postBody, fieldName); if (fieldValue) { postBody.filter_by.push({ field_name: fieldName, field_value: fieldValue, }); - } else { - removeFieldFromReportMetadata(postBody, fieldName); } }; @@ -720,24 +632,29 @@ export default function ProcessInstanceListTable({ }; } - addFieldValueToReportMetadata( + insertOrUpdateFieldInReportMetadata( newReportMetadata, 'start_from', startFromSeconds ); - addFieldValueToReportMetadata( + insertOrUpdateFieldInReportMetadata( newReportMetadata, 'start_to', startToSeconds ); - addFieldValueToReportMetadata( + insertOrUpdateFieldInReportMetadata( newReportMetadata, 'end_from', endFromSeconds ); - addFieldValueToReportMetadata(newReportMetadata, 'end_to', endToSeconds); + insertOrUpdateFieldInReportMetadata( + newReportMetadata, + 'end_to', + endToSeconds + ); + if (processStatusSelection.length > 0) { - addFieldValueToReportMetadata( + insertOrUpdateFieldInReportMetadata( newReportMetadata, 'process_status', processStatusSelection.join(',') @@ -747,7 +664,7 @@ export default function ProcessInstanceListTable({ } if (processModelSelection) { - addFieldValueToReportMetadata( + insertOrUpdateFieldInReportMetadata( newReportMetadata, 'process_model_identifier', processModelSelection.id @@ -760,10 +677,10 @@ export default function ProcessInstanceListTable({ } if (processInitiatorSelection) { - addFieldValueToReportMetadata( + insertOrUpdateFieldInReportMetadata( newReportMetadata, 'process_initiator_username', - processInitiatorSelection.username + processInitiatorSelection ); } else { removeFieldFromReportMetadata( @@ -800,7 +717,7 @@ export default function ProcessInstanceListTable({ failureCallback: stopRefreshing, onUnauthorized: stopRefreshing, successCallback: (result: any) => { - setProcessInstancesFromResult(result); + setProcessInstancesFromApplyFilter(result); }, }); }; @@ -892,7 +809,6 @@ export default function ProcessInstanceListTable({ setEndToDate(''); setEndToTime(''); setProcessInitiatorSelection(null); - setProcessInitiatorText(''); setRequiresRefilter(true); if (reportMetadata) { reportMetadata.filter_by = []; @@ -929,35 +845,18 @@ export default function ProcessInstanceListTable({ }; const saveAsReportComponent = () => { - const { - valid, - startFromSeconds, - startToSeconds, - endFromSeconds, - endToSeconds, - } = calculateStartAndEndSeconds(false); - const newReportMetadata = getNewReportMetadataBasedOnPageWidgets(); - if (!valid || !reportMetadata || !newReportMetadata) { + if (!newReportMetadata) { return null; } return ( ); }; @@ -1325,7 +1224,7 @@ export default function ProcessInstanceListTable({ items={processInstanceInitiatorOptions} itemToString={(processInstanceInitatorOption: User) => { if (processInstanceInitatorOption) { - return processInstanceInitatorOption.username; + return processInstanceInitatorOption; } return null; }} @@ -1343,7 +1242,7 @@ export default function ProcessInstanceListTable({ invalid={processInitiatorNotFoundErrorText !== ''} invalidText={processInitiatorNotFoundErrorText} onChange={(event: any) => { - setProcessInitiatorText(event.target.value); + setProcessInitiatorSelection(event.target.value); setRequiresRefilter(true); }} /> From 3754d72f4f1a1e69dc10f9fc5daf54dcb39481f7 Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 28 Apr 2023 10:07:21 -0400 Subject: [PATCH 08/18] tests are now passing and pyl w/ burnettk --- .../src/spiffworkflow_backend/api.yml | 261 ++++----------- .../models/process_instance_report.py | 32 +- .../routes/process_instances_controller.py | 10 +- .../helpers/base_test.py | 24 ++ .../integration/test_debug_controller.py | 1 - .../integration/test_process_api.py | 309 ++++++------------ .../unit/test_process_instance_processor.py | 3 - .../unit/test_process_instance_report.py | 2 +- .../components/ProcessInstanceListTable.tsx | 12 +- 9 files changed, 214 insertions(+), 440 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index f6483c39..cab7621d 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -616,107 +616,18 @@ paths: description: The page number to return. Defaults to page 1. schema: type: integer - - name: start_from - in: query - required: false - description: For filtering - beginning of start window - in seconds since epoch - schema: - type: integer - - name: start_to - in: query - required: false - description: For filtering - end of start window - in seconds since epoch - schema: - type: integer - - name: end_from - in: query - required: false - description: For filtering - beginning of end window - in seconds since epoch - schema: - type: integer - - name: end_to - in: query - required: false - description: For filtering - end of end window - in seconds since epoch - schema: - type: integer - - name: process_status - in: query - required: false - 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: with_relation_to_me - in: query - required: false - description: For filtering - show instances that have something to do with me - schema: - type: boolean - - name: user_filter - in: query - required: false - description: For filtering - indicates the user has manually entered a query - schema: - type: boolean - - name: report_identifier - in: query - required: false - description: Specifies the identifier of a report to use, if any - schema: - type: string - - name: report_id - in: query - required: false - description: Specifies the identifier of a report to use, if any - schema: - type: integer - - name: user_group_identifier - in: query - required: false - 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 - - name: report_columns - in: query - required: false - description: Base64 encoded json of report columns. - schema: - type: string - - name: report_filter_by - in: query - required: false - description: Base64 encoded json of report filter by. - schema: - type: string post: operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_list_for_me summary: Returns a list of process instances that are associated with me. tags: - Process Instances + requestBody: + description: Report dictionary to use. + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ProcessInstanceReport" responses: "200": description: Workflow. @@ -747,107 +658,18 @@ paths: description: The page number to return. Defaults to page 1. schema: type: integer - - name: start_from - in: query - required: false - description: For filtering - beginning of start window - in seconds since epoch - schema: - type: integer - - name: start_to - in: query - required: false - description: For filtering - end of start window - in seconds since epoch - schema: - type: integer - - name: end_from - in: query - required: false - description: For filtering - beginning of end window - in seconds since epoch - schema: - type: integer - - name: end_to - in: query - required: false - description: For filtering - end of end window - in seconds since epoch - schema: - type: integer - - name: process_status - in: query - required: false - 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: with_relation_to_me - in: query - required: false - description: For filtering - show instances that have something to do with me - schema: - type: boolean - - name: user_filter - in: query - required: false - description: For filtering - indicates the user has manually entered a query - schema: - type: boolean - - name: report_identifier - in: query - required: false - description: Specifies the identifier of a report to use, if any - schema: - type: string - - name: report_id - in: query - required: false - description: Specifies the identifier of a report to use, if any - schema: - type: integer - - name: user_group_identifier - in: query - required: false - 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 - - name: report_columns - in: query - required: false - description: Base64 encoded json of report columns. - schema: - type: string - - name: report_filter_by - in: query - required: false - description: Base64 encoded json of report filter by. - schema: - type: string post: operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_list summary: Returns a list of process instances. tags: - Process Instances + requestBody: + description: Report dictionary to use. + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ProcessInstanceReport" responses: "200": description: Workflow. @@ -1268,15 +1090,16 @@ paths: summary: Returns all process instance reports for process model tags: - Process Instances + - Process Instances Reports responses: "200": - description: Workflow. + description: Process Instance Report content: application/json: schema: type: array items: - $ref: "#/components/schemas/Workflow" + $ref: "#/components/schemas/ProcessInstanceReport" post: operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_report_create summary: Returns all process instance reports for process model @@ -3098,3 +2921,53 @@ components: description: The timestamp returned in the log type: number example: 123456789.12345 + + ProcessInstanceReport: + properties: + id: + type: number + nullable: true + identifier: + type: string + nullable: true + name: + type: string + nullable: true + report_metadata: + nullable: false + $ref: "#/components/schemas/ReportMetadata" + ReportMetadata: + properties: + columns: + type: array + nullable: false + $ref: "#/components/schemas/ReportMetadataColumn" + filter_by: + type: array + nullable: false + $ref: "#/components/schemas/FilterValue" + order_by: + type: array + nullable: false + ReportMetadataColumn: + properties: + Header: + type: string + nullable: false + accessor: + type: string + nullable: false + fiilterable: + type: boolean + nullable: true + FilterValue: + properties: + field_name: + type: string + nullable: false + field_value: + type: string + nullable: false + fiilterable: + type: string + nullable: false diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py index 74ee1a48..e5b8c5c6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py @@ -40,6 +40,13 @@ class ReportMetadata(TypedDict): order_by: list[str] +class Report(TypedDict): + id: int + identifier: str + name: str + report_metadata: ReportMetadata + + class ProcessInstanceReportAlreadyExistsError(Exception): """ProcessInstanceReportAlreadyExistsError.""" @@ -91,8 +98,10 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): json_data_hash: str = db.Column(db.String(255), nullable=False, index=True) - def get_report_metadata(self) -> dict: - return JsonDataModel.find_data_dict_by_hash(self.json_data_hash) + def get_report_metadata(self) -> ReportMetadata: + rdata_dict = JsonDataModel.find_data_dict_by_hash(self.json_data_hash) + rdata = typing.cast(ReportMetadata, rdata_dict) + return rdata @classmethod def default_order_by(cls) -> list[str]: @@ -106,7 +115,6 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): user: UserModel, report_metadata: ReportMetadata, ) -> ProcessInstanceReportModel: - """Make_fixture_report.""" process_instance_report = ProcessInstanceReportModel.query.filter_by( identifier=identifier, created_by_id=user.id, @@ -131,25 +139,7 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): return process_instance_report # type: ignore - @classmethod - def create_with_attributes( - cls, - identifier: str, - report_metadata: ReportMetadata, - user: UserModel, - ) -> ProcessInstanceReportModel: - """Create_with_attributes.""" - process_instance_report = cls( - identifier=identifier, - created_by_id=user.id, - report_metadata=report_metadata, - ) - db.session.add(process_instance_report) - db.session.commit() - return process_instance_report - def with_substitutions(self, field_value: Any, substitution_variables: dict) -> Any: - """With_substitutions.""" if substitution_variables is not None: for key, value in substitution_variables.items(): if isinstance(value, str) or isinstance(value, int): 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 dd249372..ed02134c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -3,6 +3,7 @@ import json from typing import Any from typing import Dict from typing import Optional +from typing import Union import flask.wrappers from flask import current_app @@ -35,9 +36,8 @@ from spiffworkflow_backend.models.process_instance_metadata import ( from spiffworkflow_backend.models.process_instance_queue import ( ProcessInstanceQueueModel, ) -from spiffworkflow_backend.models.process_instance_report import ( - ProcessInstanceReportModel, -) +from spiffworkflow_backend.models.process_instance_report import ProcessInstanceReportModel +from spiffworkflow_backend.models.process_instance_report import Report from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.spec_reference import SpecReferenceNotFoundError @@ -231,7 +231,6 @@ def process_instance_list_for_me( page: int = 1, per_page: int = 100, ) -> flask.wrappers.Response: - """Process_instance_list_for_me.""" return process_instance_list( process_model_identifier=process_model_identifier, page=page, @@ -273,7 +272,7 @@ def process_instance_report_show( " report_identifier." ), ) - response_result = {} + response_result: Optional[Union[Report, ProcessInstanceReportModel]] = None if report_hash is not None: json_data = JsonDataModel.query.filter_by(hash=report_hash).first() if json_data is None: @@ -365,7 +364,6 @@ def process_instance_delete( def process_instance_report_list(page: int = 1, per_page: int = 100) -> flask.wrappers.Response: - """Process_instance_report_list.""" process_instance_reports = ProcessInstanceReportModel.query.filter_by( created_by_id=g.user.id, ).all() diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py index 03620228..d89ed358 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py @@ -19,6 +19,7 @@ from spiffworkflow_backend.models.permission_target import PermissionTargetModel from spiffworkflow_backend.models.process_group import ProcessGroup from spiffworkflow_backend.models.process_group import ProcessGroupSchema from spiffworkflow_backend.models.process_instance import ProcessInstanceModel +from spiffworkflow_backend.models.process_instance_report import ReportMetadata from spiffworkflow_backend.models.process_model import NotificationType from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema @@ -379,3 +380,26 @@ class BaseTest: }, ) return process_model + + def post_to_process_instance_list( + self, + client: FlaskClient, + user: UserModel, + report_metadata: Optional[ReportMetadata] = None, + param_string: Optional[str] = "", + ) -> TestResponse: + report_metadata_to_use = report_metadata + if report_metadata_to_use is None: + report_metadata_to_use = self.empty_report_metadata_body() + response = client.post( + f"/v1.0/process-instances{param_string}", + headers=self.logged_in_headers(user), + content_type="application/json", + data=json.dumps({"report_metadata": report_metadata_to_use}), + ) + assert response.status_code == 200 + assert response.json is not None + return response + + def empty_report_metadata_body(self) -> ReportMetadata: + return {"filter_by": [], "columns": [], "order_by": []} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_debug_controller.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_debug_controller.py index 84fcafb3..d9136020 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_debug_controller.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_debug_controller.py @@ -4,7 +4,6 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest class TestDebugController(BaseTest): - def test_test_raise_error( self, app: Flask, 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 8fec7d26..20ef475a 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -1761,12 +1761,7 @@ class TestProcessApi(BaseTest): headers = self.logged_in_headers(with_super_admin_user) self.create_process_instance_from_process_model_id_with_api(client, process_model_identifier, headers) - response = client.get( - "/v1.0/process-instances", - headers=self.logged_in_headers(with_super_admin_user), - ) - assert response.status_code == 200 - assert response.json is not None + response = self.post_to_process_instance_list(client, with_super_admin_user) assert len(response.json["results"]) == 1 assert response.json["pagination"]["count"] == 1 assert response.json["pagination"]["pages"] == 1 @@ -1807,23 +1802,13 @@ class TestProcessApi(BaseTest): self.create_process_instance_from_process_model_id_with_api(client, process_model_identifier, headers) self.create_process_instance_from_process_model_id_with_api(client, process_model_identifier, headers) - response = client.get( - "/v1.0/process-instances?per_page=2&page=3", - headers=self.logged_in_headers(with_super_admin_user), - ) - assert response.status_code == 200 - assert response.json is not None + response = self.post_to_process_instance_list(client, with_super_admin_user, param_string="?per_page=2&page=3") assert len(response.json["results"]) == 1 assert response.json["pagination"]["count"] == 1 assert response.json["pagination"]["pages"] == 3 assert response.json["pagination"]["total"] == 5 - response = client.get( - "/v1.0/process-instances?per_page=2&page=1", - headers=self.logged_in_headers(with_super_admin_user), - ) - assert response.status_code == 200 - assert response.json is not None + response = self.post_to_process_instance_list(client, with_super_admin_user, param_string="?per_page=2&page=1") assert len(response.json["results"]) == 2 assert response.json["pagination"]["count"] == 2 assert response.json["pagination"]["pages"] == 3 @@ -1867,31 +1852,46 @@ class TestProcessApi(BaseTest): db.session.commit() # Without filtering we should get all 5 instances - response = client.get( - f"/v1.0/process-instances?process_model_identifier={process_model_identifier}", - headers=self.logged_in_headers(with_super_admin_user), + report_metadata_body: ReportMetadata = { + "filter_by": [{"field_name": "process_model_identifier", "field_value": process_model_identifier}], + "columns": [], + "order_by": [], + } + response = self.post_to_process_instance_list( + client, with_super_admin_user, report_metadata=report_metadata_body ) - assert response.json is not None results = response.json["results"] assert len(results) == 5 # filter for each of the status # we should get 1 instance each time for i in range(5): - response = client.get( - f"/v1.0/process-instances?process_status={ProcessInstanceStatus[statuses[i]].value}&process_model_identifier={process_model_identifier}", - headers=self.logged_in_headers(with_super_admin_user), + report_metadata_body = { + "filter_by": [ + {"field_name": "process_model_identifier", "field_value": process_model_identifier}, + {"field_name": "process_status", "field_value": ProcessInstanceStatus[statuses[i]].value}, + ], + "columns": [], + "order_by": [], + } + response = self.post_to_process_instance_list( + client, with_super_admin_user, report_metadata=report_metadata_body ) - assert response.json is not None results = response.json["results"] assert len(results) == 1 assert results[0]["status"] == ProcessInstanceStatus[statuses[i]].value - response = client.get( - f"/v1.0/process-instances?process_status=not_started,complete&process_model_identifier={process_model_identifier}", - headers=self.logged_in_headers(with_super_admin_user), + report_metadata_body = { + "filter_by": [ + {"field_name": "process_model_identifier", "field_value": process_model_identifier}, + {"field_name": "process_status", "field_value": "not_started,complete"}, + ], + "columns": [], + "order_by": [], + } + response = self.post_to_process_instance_list( + client, with_super_admin_user, report_metadata=report_metadata_body ) - assert response.json is not None results = response.json["results"] assert len(results) == 2 assert results[0]["status"] in ["complete", "not_started"] @@ -1899,11 +1899,14 @@ class TestProcessApi(BaseTest): # filter by start/end seconds # start > 1000 - this should eliminate the first - response = client.get( - "/v1.0/process-instances?start_from=1001", - headers=self.logged_in_headers(with_super_admin_user), + report_metadata_body = { + "filter_by": [{"field_name": "start_from", "field_value": 1001}], + "columns": [], + "order_by": [], + } + response = self.post_to_process_instance_list( + client, with_super_admin_user, report_metadata=report_metadata_body ) - assert response.json is not None results = response.json["results"] assert len(results) == 4 for i in range(4): @@ -1915,33 +1918,51 @@ class TestProcessApi(BaseTest): ) # start > 2000, end < 5000 - this should eliminate the first 2 and the last - response = client.get( - "/v1.0/process-instances?start_from=2001&end_to=5999", - headers=self.logged_in_headers(with_super_admin_user), + report_metadata_body = { + "filter_by": [ + {"field_name": "start_from", "field_value": 2001}, + {"field_name": "end_to", "field_value": 5999}, + ], + "columns": [], + "order_by": [], + } + response = self.post_to_process_instance_list( + client, with_super_admin_user, report_metadata=report_metadata_body ) - assert response.json is not None results = response.json["results"] assert len(results) == 2 assert json.loads(results[0]["bpmn_version_control_identifier"]) in (2, 3) assert json.loads(results[1]["bpmn_version_control_identifier"]) in (2, 3) # start > 1000, start < 4000 - this should eliminate the first and the last 2 - response = client.get( - "/v1.0/process-instances?start_from=1001&start_to=3999", - headers=self.logged_in_headers(with_super_admin_user), + report_metadata_body = { + "filter_by": [ + {"field_name": "start_from", "field_value": 1001}, + {"field_name": "start_to", "field_value": 3999}, + ], + "columns": [], + "order_by": [], + } + response = self.post_to_process_instance_list( + client, with_super_admin_user, report_metadata=report_metadata_body ) - assert response.json is not None results = response.json["results"] assert len(results) == 2 assert json.loads(results[0]["bpmn_version_control_identifier"]) in (1, 2) assert json.loads(results[1]["bpmn_version_control_identifier"]) in (1, 2) # end > 2000, end < 6000 - this should eliminate the first and the last - response = client.get( - "/v1.0/process-instances?end_from=2001&end_to=5999", - headers=self.logged_in_headers(with_super_admin_user), + report_metadata_body = { + "filter_by": [ + {"field_name": "end_from", "field_value": 2001}, + {"field_name": "end_to", "field_value": 5999}, + ], + "columns": [], + "order_by": [], + } + response = self.post_to_process_instance_list( + client, with_super_admin_user, report_metadata=report_metadata_body ) - assert response.json is not None results = response.json["results"] assert len(results) == 3 for i in range(3): @@ -1975,7 +1996,7 @@ class TestProcessApi(BaseTest): report_identifier = "testreport" report_metadata: ReportMetadata = {"order_by": ["month"], "filter_by": [], "columns": []} - ProcessInstanceReportModel.create_with_attributes( + ProcessInstanceReportModel.create_report( identifier=report_identifier, report_metadata=report_metadata, user=with_super_admin_user, @@ -1990,129 +2011,6 @@ class TestProcessApi(BaseTest): assert response.json[0]["identifier"] == report_identifier assert response.json[0]["report_metadata"]["order_by"] == ["month"] - # def test_process_instance_report_show_with_default_list( - # self, - # app: Flask, - # client: FlaskClient, - # with_db_and_bpmn_file_cleanup: None, - # with_super_admin_user: UserModel, - # setup_process_instances_for_reports: list[ProcessInstanceModel], - # ) -> None: - # """Test_process_instance_report_show_with_default_list.""" - # process_group_id = "runs_without_input" - # process_model_id = "sample" - # process_model_identifier = f"{process_group_id}/{process_model_id}" - # - # report_metadata = { - # "columns": [ - # {"Header": "id", "accessor": "id"}, - # { - # "Header": "process_model_identifier", - # "accessor": "process_model_identifier", - # }, - # {"Header": "process_group_id", "accessor": "process_group_identifier"}, - # {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, - # {"Header": "status", "accessor": "status"}, - # {"Header": "Name", "accessor": "name"}, - # {"Header": "Status", "accessor": "status"}, - # ], - # "order_by": ["test_score"], - # "filter_by": [ - # {"field_name": "grade_level", "operator": "equals", "field_value": 2} - # ], - # } - # - # report = ProcessInstanceReportModel.create_with_attributes( - # identifier="sure", - # report_metadata=report_metadata, - # user=with_super_admin_user, - # ) - # - # response = client.get( - # f"/v1.0/process-instances/reports/{report.id}", - # headers=self.logged_in_headers(with_super_admin_user), - # ) - # assert response.status_code == 200 - # assert response.json is not None - # assert len(response.json["results"]) == 2 - # assert response.json["pagination"]["count"] == 2 - # assert response.json["pagination"]["pages"] == 1 - # assert response.json["pagination"]["total"] == 2 - # - # process_instance_dict = response.json["results"][0] - # assert type(process_instance_dict["id"]) is int - # assert ( - # process_instance_dict["process_model_identifier"] - # == process_model_identifier - # ) - # assert type(process_instance_dict["start_in_seconds"]) is int - # assert process_instance_dict["start_in_seconds"] > 0 - # assert process_instance_dict["status"] == "complete" - # - # def test_process_instance_report_show_with_dynamic_filter_and_query_param( - # self, - # app: Flask, - # client: FlaskClient, - # with_db_and_bpmn_file_cleanup: None, - # with_super_admin_user: UserModel, - # setup_process_instances_for_reports: list[ProcessInstanceModel], - # ) -> None: - # """Test_process_instance_report_show_with_default_list.""" - # report_metadata = { - # "filter_by": [ - # { - # "field_name": "grade_level", - # "operator": "equals", - # "field_value": "{{grade_level}}", - # } - # ], - # } - # - # report = ProcessInstanceReportModel.create_with_attributes( - # identifier="sure", - # report_metadata=report_metadata, - # user=with_super_admin_user, - # ) - # - # response = client.get( - # f"/v1.0/process-instances/reports/{report.id}?grade_level=1", - # headers=self.logged_in_headers(with_super_admin_user), - # ) - # assert response.status_code == 200 - # assert response.json is not None - # assert len(response.json["results"]) == 1 - - def test_process_instance_report_show_with_bad_identifier( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - setup_process_instances_for_reports: list[ProcessInstanceModel], - ) -> None: - """Test_process_instance_report_show_with_bad_identifier.""" - response = client.get( - "/v1.0/process-instances/reports/13000000?grade_level=1", - headers=self.logged_in_headers(with_super_admin_user), - ) - assert response.status_code == 404 - data = json.loads(response.get_data(as_text=True)) - assert data["error_code"] == "unknown_process_instance_report" - - def setup_testing_instance( - self, - client: FlaskClient, - process_model_id: str, - with_super_admin_user: UserModel, - ) -> Any: - """Setup_testing_instance.""" - headers = self.logged_in_headers(with_super_admin_user) - response = self.create_process_instance_from_process_model_id_with_api(client, process_model_id, headers) - process_instance = response.json - assert isinstance(process_instance, dict) - process_instance_id = process_instance["id"] - return process_instance_id - def test_error_handler( self, app: Flask, @@ -2134,7 +2032,7 @@ class TestProcessApi(BaseTest): bpmn_file_location=bpmn_file_location, ) - process_instance_id = self.setup_testing_instance(client, process_model_identifier, with_super_admin_user) + process_instance_id = self._setup_testing_instance(client, process_model_identifier, with_super_admin_user) process = db.session.query(ProcessInstanceModel).filter(ProcessInstanceModel.id == process_instance_id).first() assert process is not None @@ -2175,7 +2073,7 @@ class TestProcessApi(BaseTest): bpmn_file_location=bpmn_file_location, ) - process_instance_id = self.setup_testing_instance(client, process_model_identifier, with_super_admin_user) + process_instance_id = self._setup_testing_instance(client, process_model_identifier, with_super_admin_user) process_model = ProcessModelService.get_process_model(process_model_identifier) ProcessModelService.update_process_model( process_model, @@ -2683,8 +2581,9 @@ class TestProcessApi(BaseTest): content_type="application/json", data=json.dumps(data), ) - - print("test_script_unit_test_run") + # TODO: fix this test. I'm not sure it ever worked since it used to NOT check the status code + # and only printed out the test name. + assert response.status_code == 404 def test_send_event( self, @@ -3060,7 +2959,6 @@ class TestProcessApi(BaseTest): with_db_and_bpmn_file_cleanup: None, with_super_admin_user: UserModel, ) -> None: - """Test_can_get_process_instance_list_with_report_metadata.""" process_model = load_test_spec( process_model_id="save_process_instance_metadata/save_process_instance_metadata", bpmn_file_name="save_process_instance_metadata.bpmn", @@ -3087,20 +2985,16 @@ class TestProcessApi(BaseTest): "order_by": ["status"], "filter_by": [], } - process_instance_report = ProcessInstanceReportModel.create_with_attributes( + process_instance_report = ProcessInstanceReportModel.create_report( identifier="sure", report_metadata=report_metadata, user=with_super_admin_user, ) - response = client.get( - f"/v1.0/process-instances?report_identifier={process_instance_report.identifier}", - headers=self.logged_in_headers(with_super_admin_user), + response = self.post_to_process_instance_list( + client, with_super_admin_user, report_metadata=process_instance_report.get_report_metadata() ) - assert response.json is not None - assert response.status_code == 200 - assert len(response.json["results"]) == 1 assert response.json["results"][0]["status"] == "complete" assert response.json["results"][0]["id"] == process_instance.id @@ -3160,33 +3054,27 @@ class TestProcessApi(BaseTest): } ], } - process_instance_report_dne = ProcessInstanceReportModel.create_with_attributes( + process_instance_report_dne = ProcessInstanceReportModel.create_report( identifier="dne_report", report_metadata=dne_report_metadata, user=user_one, ) - process_instance_report_user_one = ProcessInstanceReportModel.create_with_attributes( + process_instance_report_user_one = ProcessInstanceReportModel.create_report( 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), + response = self.post_to_process_instance_list( + client, user_one, report_metadata=process_instance_report_user_one.get_report_metadata() ) - assert response.json is not None - assert response.status_code == 200 assert len(response.json["results"]) == 2 assert response.json["results"][0]["process_initiator_username"] == user_one.username assert response.json["results"][1]["process_initiator_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), + response = self.post_to_process_instance_list( + client, user_one, report_metadata=process_instance_report_dne.get_report_metadata() ) - 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( @@ -3196,7 +3084,6 @@ class TestProcessApi(BaseTest): with_db_and_bpmn_file_cleanup: None, with_super_admin_user: UserModel, ) -> None: - """Test_can_get_process_instance_list_with_report_metadata.""" process_model = self.create_process_model_with_metadata() process_instance = self.create_process_instance_from_process_model( process_model=process_model, user=with_super_admin_user @@ -3315,18 +3202,15 @@ class TestProcessApi(BaseTest): "order_by": ["time_ns"], "filter_by": [], } - report_one = ProcessInstanceReportModel.create_with_attributes( + report_one = ProcessInstanceReportModel.create_report( identifier="report_one", report_metadata=report_metadata, user=with_super_admin_user, ) - response = client.get( - f"/v1.0/process-instances?report_id={report_one.id}", - headers=self.logged_in_headers(with_super_admin_user), + response = self.post_to_process_instance_list( + client, with_super_admin_user, report_metadata=report_one.get_report_metadata() ) - assert response.status_code == 200 - assert response.json is not None assert len(response.json["results"]) == 2 assert response.json["results"][0]["id"] == process_instance_one.id assert response.json["results"][1]["id"] == process_instance_two.id @@ -3339,19 +3223,15 @@ class TestProcessApi(BaseTest): "order_by": ["-time_ns"], "filter_by": [], } - report_two = ProcessInstanceReportModel.create_with_attributes( + report_two = ProcessInstanceReportModel.create_report( identifier="report_two", report_metadata=report_metadata, user=with_super_admin_user, ) - response = client.get( - f"/v1.0/process-instances?report_id={report_two.id}", - headers=self.logged_in_headers(with_super_admin_user), + response = self.post_to_process_instance_list( + client, with_super_admin_user, report_metadata=report_two.get_report_metadata() ) - - assert response.status_code == 200 - assert response.json is not None assert len(response.json["results"]) == 2 assert response.json["results"][1]["id"] == process_instance_one.id assert response.json["results"][0]["id"] == process_instance_two.id @@ -3381,3 +3261,16 @@ class TestProcessApi(BaseTest): assert response.status_code == 200 assert response.json is not None assert response.json["process_data_value"] == "hey" + + def _setup_testing_instance( + self, + client: FlaskClient, + process_model_id: str, + with_super_admin_user: UserModel, + ) -> Any: + headers = self.logged_in_headers(with_super_admin_user) + response = self.create_process_instance_from_process_model_id_with_api(client, process_model_id, headers) + process_instance = response.json + assert isinstance(process_instance, dict) + process_instance_id = process_instance["id"] + return process_instance_id diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py index e89a959a..bfda1eb3 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py @@ -362,9 +362,6 @@ class TestProcessInstanceProcessor(BaseTest): human_task_one = process_instance.active_human_tasks[0] spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id)) ProcessInstanceService.complete_form_task(processor, spiff_manual_task, {}, initiator_user, human_task_one) - import pdb - - pdb.set_trace() assert ( len(process_instance.active_human_tasks) == 1 ), "expected 1 active human tasks after 2nd one is completed, as we have looped back around." diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report.py index 51b3d222..64c7f3f7 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report.py @@ -125,7 +125,7 @@ # substitution_variables: Optional[dict] = None, # ) -> list[dict]: # """Do_report_with_metadata_and_instances.""" -# process_instance_report = ProcessInstanceReportModel.create_with_attributes( +# process_instance_report = ProcessInstanceReportModel.create_report( # identifier="sure", # report_metadata=report_metadata, # user=BaseTest.find_or_create_user(), diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 7284eaf6..b3b496d5 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -583,24 +583,24 @@ export default function ProcessInstanceListTable({ }; const removeFieldFromReportMetadata = ( - postBody: ReportMetadata, + reportMetadataToUse: ReportMetadata, fieldName: string ) => { - const filtersToKeep = postBody.filter_by.filter( + const filtersToKeep = reportMetadataToUse.filter_by.filter( (rf: ReportFilter) => rf.field_name !== fieldName ); // eslint-disable-next-line no-param-reassign - postBody.filter_by = filtersToKeep; + reportMetadataToUse.filter_by = filtersToKeep; }; const insertOrUpdateFieldInReportMetadata = ( - postBody: ReportMetadata, + reportMetadataToUse: ReportMetadata, fieldName: string, fieldValue: string ) => { - removeFieldFromReportMetadata(postBody, fieldName); + removeFieldFromReportMetadata(reportMetadataToUse, fieldName); if (fieldValue) { - postBody.filter_by.push({ + reportMetadataToUse.filter_by.push({ field_name: fieldName, field_value: fieldValue, }); From 80ec1194db2ead4a7225c6dd5da2cef3338ea1f3 Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 28 Apr 2023 11:56:52 -0400 Subject: [PATCH 09/18] fixed cypress process instance tests w/ burnettk --- .../cypress/e2e/process_instances.cy.js | 10 ++++++++-- .../ProcessInstanceListSaveAsReport.tsx | 9 +++++++-- .../components/ProcessInstanceListTable.tsx | 19 +++++++------------ spiffworkflow-frontend/src/helpers.tsx | 4 ++++ 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/spiffworkflow-frontend/cypress/e2e/process_instances.cy.js b/spiffworkflow-frontend/cypress/e2e/process_instances.cy.js index aa0c6626..db6f53d3 100644 --- a/spiffworkflow-frontend/cypress/e2e/process_instances.cy.js +++ b/spiffworkflow-frontend/cypress/e2e/process_instances.cy.js @@ -1,5 +1,6 @@ import { format } from 'date-fns'; import { DATE_FORMAT, PROCESS_STATUSES } from '../../src/config'; +import { titleizeString } from '../../src/helpers'; const filterByDate = (fromDate) => { cy.get('#date-picker-start-from').clear().type(format(fromDate, DATE_FORMAT)); @@ -172,14 +173,19 @@ describe('process-instances', () => { cy.contains('All Process Instances'); cy.assertAtLeastOneItemInPaginatedResults(); + cy.getBySel('filter-section-expand-toggle').click(); + const statusSelect = '#process-instance-status-select'; PROCESS_STATUSES.forEach((processStatus) => { if (!['all', 'waiting'].includes(processStatus)) { cy.get(statusSelect).click(); - cy.get(statusSelect).contains(processStatus).click(); + cy.get(statusSelect).contains(titleizeString(processStatus)).click(); cy.get(statusSelect).click(); cy.getBySel('filter-button').click(); - cy.url().should('include', `status=${processStatus}`); + + // make sure that there is 1 status item selected in the multiselect + cy.get(`${statusSelect} .cds--tag`).contains('1'); + cy.assertAtLeastOneItemInPaginatedResults(); cy.getBySel(`process-instance-status-${processStatus}`); diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx index b196c2ee..9f912628 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx @@ -14,7 +14,7 @@ type OwnProps = { buttonText?: string; buttonClassName?: string; processInstanceReportSelection?: ProcessInstanceReport | null; - reportMetadata: ReportMetadata; + getReportMetadataCallback: Function; }; export default function ProcessInstanceListSaveAsReport({ @@ -22,7 +22,7 @@ export default function ProcessInstanceListSaveAsReport({ processInstanceReportSelection, buttonClassName, buttonText = 'Save as Perspective', - reportMetadata, + getReportMetadataCallback, }: OwnProps) { const [identifier, setIdentifier] = useState( processInstanceReportSelection?.identifier || '' @@ -50,6 +50,11 @@ export default function ProcessInstanceListSaveAsReport({ const addProcessInstanceReport = (event: any) => { event.preventDefault(); + const reportMetadata = getReportMetadataCallback(); + if (!reportMetadata) { + return; + } + let path = `/process-instances/reports`; let httpMethod = 'POST'; if (isEditMode() && processInstanceReportSelection) { diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index b3b496d5..1bb76de9 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -31,7 +31,6 @@ import { DATE_FORMAT_FOR_DISPLAY, } from '../config'; import { - capitalizeFirstLetter, convertDateAndTimeStringsToSeconds, convertDateObjectToFormattedHoursMinutes, convertSecondsToFormattedDateString, @@ -42,6 +41,7 @@ import { refreshAtInterval, REFRESH_INTERVAL_SECONDS, REFRESH_TIMEOUT_SECONDS, + titleizeString, } from '../helpers'; import { useUriListForPermissions } from '../hooks/UriListForPermissions'; @@ -337,10 +337,8 @@ export default function ProcessInstanceListTable({ setProcessStatusSelection( (reportFilter.field_value || '').split(',') ); - setShowFilterOptions(true); } else if (reportFilter.field_name === 'process_initiator_username') { setProcessInitiatorSelection(reportFilter.field_value || ''); - setShowFilterOptions(true); } else if (reportFilter.field_name === 'process_model_identifier') { selectedProcessModelIdentifier = reportFilter.field_value || undefined; @@ -358,7 +356,6 @@ export default function ProcessInstanceListTable({ reportFilter.field_value as any ); timeFunctionToCall(timeString); - setShowFilterOptions(true); } } } @@ -368,11 +365,14 @@ export default function ProcessInstanceListTable({ (processModel: ProcessModel) => { if (processModel.id === selectedProcessModelIdentifier) { setProcessModelSelection(processModel); - setShowFilterOptions(true); } } ); + if (reportMetadataBodyToUse.filter_by.length > 1) { + setShowFilterOptions(true); + } + // eslint-disable-next-line prefer-const let { page, perPage } = getPageInfoFromSearchParams( searchParams, @@ -774,7 +774,7 @@ export default function ProcessInstanceListTable({ }; const formatProcessInstanceStatus = (_row: any, value: any) => { - return capitalizeFirstLetter((value || '').replaceAll('_', ' ')); + return titleizeString((value || '').replaceAll('_', ' ')); }; const processStatusSearch = () => { return ( @@ -845,18 +845,13 @@ export default function ProcessInstanceListTable({ }; const saveAsReportComponent = () => { - const newReportMetadata = getNewReportMetadataBasedOnPageWidgets(); - - if (!newReportMetadata) { - return null; - } return ( ); }; diff --git a/spiffworkflow-frontend/src/helpers.tsx b/spiffworkflow-frontend/src/helpers.tsx index 441d3c0f..08aff8cf 100644 --- a/spiffworkflow-frontend/src/helpers.tsx +++ b/spiffworkflow-frontend/src/helpers.tsx @@ -41,6 +41,10 @@ export const capitalizeFirstLetter = (string: any) => { return string.charAt(0).toUpperCase() + string.slice(1); }; +export const titleizeString = (string: any) => { + return capitalizeFirstLetter((string || '').replaceAll('_', ' ')); +}; + export const convertDateToSeconds = ( date: any, onChangeFunction: any = null From b179b66e2619ff2d255a847d89eb548d853f072a Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 28 Apr 2023 12:36:16 -0400 Subject: [PATCH 10/18] home page tables are working again w/ burnettk --- .../process_instance_report_service.py | 2 +- .../ProcessInstanceListSaveAsReport.tsx | 2 +- .../components/ProcessInstanceListTable.tsx | 33 +++++++++++-------- .../src/routes/CompletedInstances.tsx | 4 ++- .../src/routes/InProgressInstances.tsx | 4 ++- 5 files changed, 28 insertions(+), 17 deletions(-) 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 a95cec34..dee7ea09 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 @@ -527,7 +527,7 @@ class ProcessInstanceReportService: results = cls.add_metadata_columns_to_process_instance(process_instances.items, report_metadata["columns"]) for value in cls.check_filter_value(filters, "oldest_open_human_task_fields"): - results = cls.add_human_task_fields(results, value) + results = cls.add_human_task_fields(results, value.split(",")) response_json = { "report_metadata": report_metadata, "results": results, diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx index 9f912628..6e43b212 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx @@ -6,7 +6,7 @@ import { Modal, // @ts-ignore } from '@carbon/react'; -import { ProcessInstanceReport, ReportMetadata } from '../interfaces'; +import { ProcessInstanceReport } from '../interfaces'; import HttpService from '../services/HttpService'; type OwnProps = { diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 1bb76de9..4430a500 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -87,7 +87,7 @@ type OwnProps = { textToShowIfEmpty?: string; paginationClassName?: string; autoReload?: boolean; - additionalParams?: string; + additionalReportFilters?: ReportFilter[]; variant?: string; canCompleteAllTasks?: boolean; showActionsColumn?: boolean; @@ -102,7 +102,7 @@ export default function ProcessInstanceListTable({ processModelFullIdentifier, paginationQueryParamPrefix, perPageOptions, - additionalParams, + additionalReportFilters, showReports = true, reportIdentifier, textToShowIfEmpty, @@ -330,6 +330,9 @@ export default function ProcessInstanceListTable({ } } + // this is the code to re-populate the widgets on the page + // with values from the report metadata, which is derived + // from the searchParams (often report_hash) let selectedProcessModelIdentifier = processModelFullIdentifier; reportMetadataBodyToUse.filter_by.forEach( (reportFilter: ReportFilter) => { @@ -360,7 +363,6 @@ export default function ProcessInstanceListTable({ } } ); - processModelSelectionItemsForUseEffect.forEach( (processModel: ProcessModel) => { if (processModel.id === selectedProcessModelIdentifier) { @@ -384,9 +386,13 @@ export default function ProcessInstanceListTable({ // eslint-disable-next-line prefer-destructuring perPage = perPageOptions[1]; } - let queryParamString = `per_page=${perPage}&page=${page}`; - if (additionalParams) { - queryParamString += `&${additionalParams}`; + const queryParamString = `per_page=${perPage}&page=${page}`; + if (additionalReportFilters) { + additionalReportFilters.forEach((arf: ReportFilter) => { + if (!reportMetadataBodyToUse.filter_by.includes(arf)) { + reportMetadataBodyToUse.filter_by.push(arf); + } + }); } HttpService.makeCallToBackend({ @@ -405,13 +411,14 @@ export default function ProcessInstanceListTable({ return; } const queryParams: string[] = []; - ['report_hash', 'report_id', 'report_identifier'].forEach( - (paramName: string) => { - if (searchParams.get(paramName)) { - queryParams.push(`${paramName}=${searchParams.get(paramName)}`); - } + ['report_hash', 'report_id'].forEach((paramName: string) => { + if (searchParams.get(paramName)) { + queryParams.push(`${paramName}=${searchParams.get(paramName)}`); } - ); + }); + if (reportIdentifier) { + queryParams.push(`report_identifier=${reportIdentifier}`); + } if (queryParams.length > 0) { const queryParamString = `?${queryParams.join('&')}`; @@ -476,7 +483,7 @@ export default function ProcessInstanceListTable({ processModelFullIdentifier, perPageOptions, reportIdentifier, - additionalParams, + additionalReportFilters, processInstanceApiSearchPath, permissionsLoaded, listHasBeenFiltered, diff --git a/spiffworkflow-frontend/src/routes/CompletedInstances.tsx b/spiffworkflow-frontend/src/routes/CompletedInstances.tsx index 6d43020d..581dbe04 100644 --- a/spiffworkflow-frontend/src/routes/CompletedInstances.tsx +++ b/spiffworkflow-frontend/src/routes/CompletedInstances.tsx @@ -32,7 +32,9 @@ export default function CompletedInstances() { reportIdentifier="system_report_completed_instances_with_tasks_completed_by_my_groups" showReports={false} textToShowIfEmpty="This group has no completed instances at this time." - additionalParams={`user_group_identifier=${userGroup}`} + additionalReportFilters={[ + { field_name: 'user_group_identifier', field_value: userGroup }, + ]} showActionsColumn /> diff --git a/spiffworkflow-frontend/src/routes/InProgressInstances.tsx b/spiffworkflow-frontend/src/routes/InProgressInstances.tsx index 9ba7cd26..1943e67d 100644 --- a/spiffworkflow-frontend/src/routes/InProgressInstances.tsx +++ b/spiffworkflow-frontend/src/routes/InProgressInstances.tsx @@ -35,7 +35,9 @@ export default function InProgressInstances() { reportIdentifier="system_report_in_progress_instances_with_tasks_for_my_group" showReports={false} textToShowIfEmpty="This group has no instances waiting on it at this time." - additionalParams={`user_group_identifier=${userGroup}`} + additionalReportFilters={[ + { field_name: 'user_group_identifier', field_value: userGroup }, + ]} canCompleteAllTasks showActionsColumn autoReload From b01b3982876973830952afe70d6a2b59b95f091a Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 28 Apr 2023 14:29:56 -0400 Subject: [PATCH 11/18] added tmp link to homepage table w/ burnettk --- .../routes/process_instances_controller.py | 1 + .../process_instance_report_service.py | 37 ++++++++++++------- .../components/ProcessInstanceListTable.tsx | 24 +++++++++++- .../src/routes/InProgressInstances.tsx | 7 +++- 4 files changed, 53 insertions(+), 16 deletions(-) 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 ed02134c..9f849bd8 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -231,6 +231,7 @@ def process_instance_list_for_me( page: int = 1, per_page: int = 100, ) -> flask.wrappers.Response: + ProcessInstanceReportService.add_or_update_filter(body['report_metadata']['filter_by'], {"field_name": 'with_relation_to_me', "field_value": True}) return process_instance_list( process_model_identifier=process_model_identifier, page=page, 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 dee7ea09..fa342cb5 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 @@ -330,6 +330,17 @@ class ProcessInstanceReportService: if value is not None: yield value + @classmethod + def add_or_update_filter(cls, filters: list[FilterValue], new_filter: FilterValue) -> None: + filter_found = False + for filter in filters: + if filter["field_name"] == new_filter['field_name']: + filter['field_value'] = new_filter['field_value'] + filter_found = True + if filter_found is False: + filters.append(new_filter) + + @classmethod def run_process_instance_report( cls, @@ -374,7 +385,8 @@ class ProcessInstanceReportService: for value in cls.check_filter_value(filters, "initiated_by_me"): if value is True: - process_instance_query = process_instance_query.filter_by(process_initiator=user) + # process_instance_query = process_instance_query.filter_by(process_initiator=user) + cls.add_or_update_filter(filters, {'field_name': 'process_initiator_username', 'field_value': user.username}) for value in cls.check_filter_value(filters, "has_terminal_status"): if value is True: @@ -385,11 +397,12 @@ class ProcessInstanceReportService: process_instance_query = process_instance_query.filter( ProcessInstanceModel.status.not_in(ProcessInstanceModel.terminal_statuses()) # type: ignore ) - for value in cls.check_filter_value(filters, "has_active_status"): - if value is True: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(ProcessInstanceModel.active_statuses()) # type: ignore - ) + + has_active_status = cls.get_filter_value(filters, "has_active_status") + if has_active_status: + process_instance_query = process_instance_query.filter( + ProcessInstanceModel.status.in_(ProcessInstanceModel.active_statuses()) # type: ignore + ) for value in cls.check_filter_value(filters, "process_initiator_username"): initiator = UserModel.query.filter_by(username=value).first() @@ -402,8 +415,6 @@ class ProcessInstanceReportService: with_tasks_assigned_to_my_group = cls.get_filter_value(filters, "with_tasks_assigned_to_my_group") with_tasks_i_can_complete = cls.get_filter_value(filters, "with_tasks_i_can_complete") with_relation_to_me = cls.get_filter_value(filters, "with_relation_to_me") - has_active_status = cls.get_filter_value(filters, "has_active_status") - user_group_identifier = cls.get_filter_value(filters, "user_group_identifier") if ( not with_tasks_completed_by_me and not with_tasks_assigned_to_my_group @@ -436,6 +447,8 @@ class ProcessInstanceReportService: ), ) + # this excludes some tasks you can complete, because that's the way the requirements were described. + # if it's assigned to one of your groups, it does not get returned by this query. if with_tasks_i_can_complete is True: process_instance_query = process_instance_query.filter( ProcessInstanceModel.process_initiator_id != user.id @@ -445,17 +458,15 @@ class ProcessInstanceReportService: and_( HumanTaskModel.process_instance_id == ProcessInstanceModel.id, HumanTaskModel.lane_assignment_id.is_(None), # type: ignore + HumanTaskModel.completed.is_(False) # type: ignore ), ).join( HumanTaskUserModel, and_(HumanTaskUserModel.human_task_id == HumanTaskModel.id, HumanTaskUserModel.user_id == user.id), ) - if has_active_status: - process_instance_query = process_instance_query.filter( - HumanTaskModel.completed.is_(False) # type: ignore - ) if with_tasks_assigned_to_my_group is True: + user_group_identifier = cls.get_filter_value(filters, "user_group_identifier") group_model_join_conditions = [GroupModel.id == HumanTaskModel.lane_assignment_id] if user_group_identifier: group_model_join_conditions.append(GroupModel.identifier == user_group_identifier) @@ -528,10 +539,10 @@ class ProcessInstanceReportService: for value in cls.check_filter_value(filters, "oldest_open_human_task_fields"): results = cls.add_human_task_fields(results, value.split(",")) + report_metadata['filter_by'] = filters response_json = { "report_metadata": report_metadata, "results": results, - "filters": filters, "pagination": { "count": len(results), "total": process_instances.total, diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 4430a500..942d7ed8 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -1,5 +1,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { + Link, + useNavigate, + useParams, + useSearchParams, +} from 'react-router-dom'; // @ts-ignore import { Close, AddAlt } from '@carbon/icons-react'; @@ -91,6 +96,7 @@ type OwnProps = { variant?: string; canCompleteAllTasks?: boolean; showActionsColumn?: boolean; + showLinkToReport?: boolean; }; interface dateParameters { @@ -111,6 +117,7 @@ export default function ProcessInstanceListTable({ variant = 'for-me', canCompleteAllTasks = false, showActionsColumn = false, + showLinkToReport = false, }: OwnProps) { let processInstanceApiSearchPath = '/process-instances/for-me'; if (variant === 'all') { @@ -200,6 +207,8 @@ export default function ProcessInstanceListTable({ string | null >(null); + const [reportHash, setReportHash] = useState(null); + const [ processInitiatorNotFoundErrorText, setProcessInitiatorNotFoundErrorText, @@ -280,6 +289,9 @@ export default function ProcessInstanceListTable({ setPagination(result.pagination); setReportMetadata(result.report_metadata); + if (result.report_hash) { + setReportHash(result.report_hash); + } }, []); const setProcessInstancesFromApplyFilter = (result: any) => { @@ -1550,6 +1562,15 @@ export default function ProcessInstanceListTable({ return null; }; + const linkToReport = () => { + if (!showLinkToReport) { + return null; + } + return ( + Hey + ); + }; + let resultsTable = null; if (pagination && (!textToShowIfEmpty || pagination.total > 0)) { // eslint-disable-next-line prefer-const @@ -1598,6 +1619,7 @@ export default function ProcessInstanceListTable({ <> {reportColumnForm()} {processInstanceReportSaveTag()} + {linkToReport()} ); @@ -67,9 +67,11 @@ export default function InProgressInstances() { showReports={false} textToShowIfEmpty="There are no open instances you started at this time." paginationClassName="with-large-bottom-margin" + showLinkToReport showActionsColumn - autoReload + autoReload={false} /> + {/*

{groupTableComponents()} + */} ); } From 1f17e58a07a34d7ab9f8e4d7962fce5317aa1c77 Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 28 Apr 2023 15:42:35 -0400 Subject: [PATCH 12/18] only return the compiled report metadata to the frontend and remove most usages of the internal filters w/ burnettk --- .../models/process_instance.py | 5 + .../process_instance_report_service.py | 102 +++++++++--------- .../components/ProcessInstanceListTable.tsx | 5 + 3 files changed, 64 insertions(+), 48 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py index 009a7486..7f95bdca 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py @@ -163,6 +163,11 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel): def terminal_statuses(cls) -> list[str]: return ["complete", "error", "terminated"] + @classmethod + def non_terminal_statuses(cls) -> list[str]: + terminal_status_values = cls.terminal_statuses() + return [s for s in ProcessInstanceStatus.list() if s not in terminal_status_values] + @classmethod def active_statuses(cls) -> list[str]: return ["user_input_required", "waiting"] 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 fa342cb5..9df4355d 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 @@ -1,5 +1,6 @@ """Process_instance_report_service.""" import re +import copy from typing import Any from typing import Generator from typing import Optional @@ -41,8 +42,10 @@ class ProcessInstanceReportService: @classmethod def system_metadata_map(cls, metadata_key: str) -> Optional[ReportMetadata]: - """System_metadata_map.""" # TODO replace with system reports that are loaded on launch (or similar) + terminal_status_values = ','.join(ProcessInstanceModel.terminal_statuses()) + non_terminal_status_values = ','.join(ProcessInstanceModel.non_terminal_statuses()) + active_status_values = ','.join(ProcessInstanceModel.active_statuses()) default: ReportMetadata = { "columns": cls.builtin_column_options(), "filter_by": [], @@ -61,7 +64,7 @@ class ProcessInstanceReportService: ], "filter_by": [ {"field_name": "initiated_by_me", "field_value": True}, - {"field_name": "has_terminal_status", "field_value": True}, + {"field_name": "process_status", "field_value": terminal_status_values}, ], "order_by": ["-start_in_seconds", "-id"], } @@ -69,18 +72,14 @@ class ProcessInstanceReportService: "columns": cls.builtin_column_options(), "filter_by": [ {"field_name": "with_tasks_completed_by_me", "field_value": True}, - {"field_name": "has_terminal_status", "field_value": True}, + {"field_name": "process_status", "field_value": terminal_status_values}, ], "order_by": ["-start_in_seconds", "-id"], } system_report_completed_instances_with_tasks_completed_by_my_groups: ReportMetadata = { "columns": cls.builtin_column_options(), "filter_by": [ - { - "field_name": "with_tasks_assigned_to_my_group", - "field_value": True, - }, - {"field_name": "has_terminal_status", "field_value": True}, + {"field_name": "process_status", "field_value": terminal_status_values}, ], "order_by": ["-start_in_seconds", "-id"], } @@ -99,12 +98,10 @@ class ProcessInstanceReportService: ], "filter_by": [ {"field_name": "initiated_by_me", "field_value": True}, - {"field_name": "has_terminal_status", "field_value": False}, + {"field_name": "process_status", "field_value": non_terminal_status_values}, { - "field_name": "oldest_open_human_task_fields", - "field_value": ( - "task_id,task_title,task_name,potential_owner_usernames,assigned_user_group_identifier" - ), + "field_name": "with_oldest_open_task", + "field_value": True, }, ], "order_by": ["-start_in_seconds", "-id"], @@ -123,10 +120,10 @@ class ProcessInstanceReportService: ], "filter_by": [ {"field_name": "with_tasks_i_can_complete", "field_value": True}, - {"field_name": "has_active_status", "field_value": True}, + {"field_name": "process_status", "field_value": active_status_values}, { - "field_name": "oldest_open_human_task_fields", - "field_value": "task_id,task_title,task_name", + "field_name": "with_oldest_open_task", + "field_value": True, }, ], "order_by": ["-start_in_seconds", "-id"], @@ -144,15 +141,11 @@ class ProcessInstanceReportService: {"Header": "Last Updated", "accessor": "updated_at_in_seconds"}, ], "filter_by": [ + {"field_name": "process_status", "field_value": active_status_values}, { - "field_name": "with_tasks_assigned_to_my_group", + "field_name": "with_oldest_open_task", "field_value": True, }, - {"field_name": "has_active_status", "field_value": True}, - { - "field_name": "oldest_open_human_task_fields", - "field_value": "task_id,task_title,task_name", - }, ], "order_by": ["-start_in_seconds", "-id"], } @@ -178,6 +171,18 @@ class ProcessInstanceReportService: return None return temp_system_metadata_map[metadata_key] + @classmethod + def compile_report(cls, report_metadata: ReportMetadata, user: UserModel) -> None: + compiled_filters: list[FilterValue] = [] + old_filters = copy.deepcopy(report_metadata['filter_by']) + for filter in old_filters: + if filter['field_name'] == 'initiated_by_me': + compiled_filters.append({'field_name': 'process_initiator_username', 'field_value': user.username}) + else: + compiled_filters.append(filter) + + report_metadata['filter_by'] = compiled_filters + @classmethod def report_with_identifier( cls, @@ -206,6 +211,7 @@ class ProcessInstanceReportService: raise ProcessInstanceReportNotFoundError( f"Could not find a report with identifier '{report_identifier}' for user '{user.username}'" ) + cls.compile_report(report_metadata, user=user) process_instance_report = ProcessInstanceReportModel( identifier=report_identifier, @@ -238,8 +244,9 @@ class ProcessInstanceReportService: @classmethod def add_human_task_fields( - cls, process_instance_dicts: list[dict], oldest_open_human_task_fields: list + cls, process_instance_dicts: list[dict] ) -> list[dict]: + fields_to_return = ["task_id", "task_title", "task_name", "potential_owner_usernames", "assigned_user_group_identifier"] for process_instance_dict in process_instance_dicts: assigned_user = aliased(UserModel) human_task_query = ( @@ -265,7 +272,7 @@ class ProcessInstanceReportService: .first() ) if human_task is not None: - for field in oldest_open_human_task_fields: + for field in fields_to_return: process_instance_dict[field] = getattr(human_task, field) return process_instance_dicts @@ -378,25 +385,18 @@ class ProcessInstanceReportService: process_instance_query = process_instance_query.filter(ProcessInstanceModel.end_in_seconds >= value) for value in cls.check_filter_value(filters, "end_to"): process_instance_query = process_instance_query.filter(ProcessInstanceModel.end_in_seconds <= value) - for value in cls.check_filter_value(filters, "process_status"): + + process_status = cls.get_filter_value(filters, "process_status") + if process_status is not None: process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(value.split(",")) # type: ignore + ProcessInstanceModel.status.in_(process_status.split(",")) # type: ignore ) - for value in cls.check_filter_value(filters, "initiated_by_me"): - if value is True: - # process_instance_query = process_instance_query.filter_by(process_initiator=user) - cls.add_or_update_filter(filters, {'field_name': 'process_initiator_username', 'field_value': user.username}) + for _value in cls.check_filter_value(filters, "initiated_by_me"): + raise Exception("DEPRECATED: initiated_by_me") for value in cls.check_filter_value(filters, "has_terminal_status"): - if value is True: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.in_(ProcessInstanceModel.terminal_statuses()) # type: ignore - ) - elif value is False: - process_instance_query = process_instance_query.filter( - ProcessInstanceModel.status.not_in(ProcessInstanceModel.terminal_statuses()) # type: ignore - ) + raise Exception("DEPRECATED: has_terminal_status") has_active_status = cls.get_filter_value(filters, "has_active_status") if has_active_status: @@ -412,12 +412,15 @@ class ProcessInstanceReportService: process_instance_query = process_instance_query.filter_by(process_initiator_id=process_initiator_id) with_tasks_completed_by_me = cls.get_filter_value(filters, "with_tasks_completed_by_me") - with_tasks_assigned_to_my_group = cls.get_filter_value(filters, "with_tasks_assigned_to_my_group") with_tasks_i_can_complete = cls.get_filter_value(filters, "with_tasks_i_can_complete") + user_group_identifier = cls.get_filter_value(filters, "user_group_identifier") + + # builtin only - for the for-me paths with_relation_to_me = cls.get_filter_value(filters, "with_relation_to_me") + if ( not with_tasks_completed_by_me - and not with_tasks_assigned_to_my_group + and not user_group_identifier and not with_tasks_i_can_complete and with_relation_to_me is True ): @@ -465,17 +468,18 @@ class ProcessInstanceReportService: and_(HumanTaskUserModel.human_task_id == HumanTaskModel.id, HumanTaskUserModel.user_id == user.id), ) - if with_tasks_assigned_to_my_group is True: - user_group_identifier = cls.get_filter_value(filters, "user_group_identifier") + if user_group_identifier is not None: group_model_join_conditions = [GroupModel.id == HumanTaskModel.lane_assignment_id] if user_group_identifier: group_model_join_conditions.append(GroupModel.identifier == user_group_identifier) process_instance_query = process_instance_query.join(HumanTaskModel) - if has_active_status: - process_instance_query = process_instance_query.filter( - HumanTaskModel.completed.is_(False) # type: ignore - ) + if process_status is not None: + non_active_statuses = [s for s in process_status.split(',') if s not in ProcessInstanceModel.active_statuses()] + if len(non_active_statuses) == 0: + process_instance_query = process_instance_query.filter( + HumanTaskModel.completed.is_(False) # type: ignore + ) process_instance_query = process_instance_query.join(GroupModel, and_(*group_model_join_conditions)) process_instance_query = process_instance_query.join( @@ -537,8 +541,10 @@ class ProcessInstanceReportService: ) results = cls.add_metadata_columns_to_process_instance(process_instances.items, report_metadata["columns"]) - for value in cls.check_filter_value(filters, "oldest_open_human_task_fields"): - results = cls.add_human_task_fields(results, value.split(",")) + for value in cls.check_filter_value(filters, "with_oldest_open_task"): + if value is True: + results = cls.add_human_task_fields(results) + report_metadata['filter_by'] = filters response_json = { "report_metadata": report_metadata, diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 942d7ed8..0e074ef9 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -1,3 +1,8 @@ +// TODO: +// add drop down for user_group_identifier and one for the 2 system reports: +// with_tasks_completed_by_me +// with_tasks_i_can_complete +// add checkbox to show with_oldest_open_task import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Link, From 607abf97c2dbed60851999944e4be9130bda4e43 Mon Sep 17 00:00:00 2001 From: jasquat Date: Mon, 1 May 2023 12:25:22 -0400 Subject: [PATCH 13/18] added links to filtered tables on homepage and added advanced options modal to use them w/ burnettk --- .../components/ProcessInstanceListTable.tsx | 335 ++++++++++++------ spiffworkflow-frontend/src/index.css | 6 + spiffworkflow-frontend/src/interfaces.ts | 5 +- .../src/routes/InProgressInstances.tsx | 75 ++-- 4 files changed, 281 insertions(+), 140 deletions(-) diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 0e074ef9..0b36b6f1 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -1,23 +1,19 @@ -// TODO: -// add drop down for user_group_identifier and one for the 2 system reports: -// with_tasks_completed_by_me -// with_tasks_i_can_complete -// add checkbox to show with_oldest_open_task -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { - Link, - useNavigate, - useParams, - useSearchParams, -} from 'react-router-dom'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; -// @ts-ignore -import { Close, AddAlt } from '@carbon/icons-react'; +import { Close, AddAlt, ArrowRight } from '@carbon/icons-react'; import { Button, ButtonSet, DatePicker, DatePickerInput, + Dropdown, Table, Grid, Column, @@ -32,7 +28,7 @@ import { ComboBox, TextInput, FormLabel, - // @ts-ignore + Checkbox, } from '@carbon/react'; import { useDebouncedCallback } from 'use-debounce'; import { @@ -102,6 +98,7 @@ type OwnProps = { canCompleteAllTasks?: boolean; showActionsColumn?: boolean; showLinkToReport?: boolean; + headerElement?: React.ReactElement; }; interface dateParameters { @@ -123,13 +120,13 @@ export default function ProcessInstanceListTable({ canCompleteAllTasks = false, showActionsColumn = false, showLinkToReport = false, + headerElement, }: OwnProps) { let processInstanceApiSearchPath = '/process-instances/for-me'; if (variant === 'all') { processInstanceApiSearchPath = '/process-instances'; } - const params = useParams(); const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const { addError, removeError } = useAPIError(); @@ -147,8 +144,6 @@ export default function ProcessInstanceListTable({ const [reportMetadata, setReportMetadata] = useState(); const [pagination, setPagination] = useState(null); - const oneHourInSeconds = 3600; - const oneMonthInSeconds = oneHourInSeconds * 24 * 30; const [startFromDate, setStartFromDate] = useState(''); const [startToDate, setStartToDate] = useState(''); const [endFromDate, setEndFromDate] = useState(''); @@ -157,12 +152,13 @@ export default function ProcessInstanceListTable({ const [startToTime, setStartToTime] = useState(''); const [endFromTime, setEndFromTime] = useState(''); const [endToTime, setEndToTime] = useState(''); - const [showFilterOptions, setShowFilterOptions] = useState(false); const [startFromTimeInvalid, setStartFromTimeInvalid] = useState(false); const [startToTimeInvalid, setStartToTimeInvalid] = useState(false); const [endFromTimeInvalid, setEndFromTimeInvalid] = useState(false); const [endToTimeInvalid, setEndToTimeInvalid] = useState(false); + + const [showFilterOptions, setShowFilterOptions] = useState(false); const [requiresRefilter, setRequiresRefilter] = useState(false); const [lastColumnFilter, setLastColumnFilter] = useState(''); @@ -212,6 +208,18 @@ export default function ProcessInstanceListTable({ string | null >(null); + const [showAdvancedOptions, setShowAdvancedOptions] = + useState(false); + const [withOldestOpenTask, setWithOldestOpenTask] = useState(false); + const [systemReport, setSystemReport] = useState(null); + const [selectedUserGroup, setSelectedUserGroup] = useState( + null + ); + const [userGroups, setUserGroups] = useState([]); + const systemReportOptions: string[] = useMemo(() => { + return ['with_tasks_i_can_complete', 'with_tasks_completed_by_me']; + }, []); + const [reportHash, setReportHash] = useState(null); const [ @@ -274,19 +282,6 @@ export default function ProcessInstanceListTable({ 250 ); - 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 - }, []); - const setProcessInstancesFromResult = useCallback((result: any) => { setRequiresRefilter(false); const processInstancesFromApi = result.results; @@ -319,19 +314,15 @@ export default function ProcessInstanceListTable({ } }, []); - // eslint-disable-next-line sonarjs/cognitive-complexity - useEffect(() => { - if (!permissionsLoaded) { - return undefined; - } + // we apparently cannot use a state set in a useEffect from within that same useEffect + // so use a variable instead + const processModelSelectionItemsForUseEffect = useRef([]); - // we apparently cannot use a state set in a useEffect from within that same useEffect - // so use a variable instead - let processModelSelectionItemsForUseEffect: ProcessModel[] = []; - - function getProcessInstances( + const getProcessInstances = useCallback( + ( processInstanceReport: ProcessInstanceReport | null = null - ) { + // eslint-disable-next-line sonarjs/cognitive-complexity + ) => { if (listHasBeenFiltered) { return; } @@ -359,6 +350,12 @@ export default function ProcessInstanceListTable({ ); } else if (reportFilter.field_name === 'process_initiator_username') { setProcessInitiatorSelection(reportFilter.field_value || ''); + } else if (reportFilter.field_name === 'with_oldest_open_task') { + setWithOldestOpenTask(reportFilter.field_value); + } else if (reportFilter.field_name === 'user_group_identifier') { + setSelectedUserGroup(reportFilter.field_value); + } else if (systemReportOptions.includes(reportFilter.field_name)) { + setSystemReport(reportFilter.field_name); } else if (reportFilter.field_name === 'process_model_identifier') { selectedProcessModelIdentifier = reportFilter.field_value || undefined; @@ -380,7 +377,7 @@ export default function ProcessInstanceListTable({ } } ); - processModelSelectionItemsForUseEffect.forEach( + processModelSelectionItemsForUseEffect.current.forEach( (processModel: ProcessModel) => { if (processModel.id === selectedProcessModelIdentifier) { setProcessModelSelection(processModel); @@ -412,6 +409,13 @@ export default function ProcessInstanceListTable({ }); } + if (filtersEnabled) { + HttpService.makeCallToBackend({ + path: `/user-groups/for-current-user`, + successCallback: setUserGroups, + }); + } + HttpService.makeCallToBackend({ path: `${processInstanceApiSearchPath}?${queryParamString}`, successCallback: setProcessInstancesFromResult, @@ -422,7 +426,28 @@ export default function ProcessInstanceListTable({ report_metadata: reportMetadataBodyToUse, }, }); + }, + [ + additionalReportFilters, + dateParametersToAlwaysFilterBy, + filtersEnabled, + listHasBeenFiltered, + paginationQueryParamPrefix, + perPageOptions, + processInstanceApiSearchPath, + processModelFullIdentifier, + searchParams, + setProcessInstancesFromResult, + stopRefreshing, + systemReportOptions, + ] + ); + + useEffect(() => { + if (!permissionsLoaded) { + return undefined; } + function getReportMetadataWithReportHash() { if (listHasBeenFiltered) { return; @@ -453,7 +478,7 @@ export default function ProcessInstanceListTable({ Object.assign(item, { label }); return item; }); - processModelSelectionItemsForUseEffect = selectionArray; + processModelSelectionItemsForUseEffect.current = selectionArray; setProcessModelAvailableItems(selectionArray); const processStatusAllOptionsArray = PROCESS_STATUSES.map( @@ -489,23 +514,12 @@ export default function ProcessInstanceListTable({ return undefined; }, [ autoReload, - searchParams, - params, - oneMonthInSeconds, - oneHourInSeconds, - dateParametersToAlwaysFilterBy, - parametersToGetFromSearchParams, filtersEnabled, - paginationQueryParamPrefix, - processModelFullIdentifier, - perPageOptions, - reportIdentifier, - additionalReportFilters, - processInstanceApiSearchPath, - permissionsLoaded, + getProcessInstances, listHasBeenFiltered, - setProcessInstancesFromResult, - stopRefreshing, + permissionsLoaded, + reportIdentifier, + searchParams, ]); const processInstanceReportSaveTag = () => { @@ -620,7 +634,7 @@ export default function ProcessInstanceListTable({ const insertOrUpdateFieldInReportMetadata = ( reportMetadataToUse: ReportMetadata, fieldName: string, - fieldValue: string + fieldValue: any ) => { removeFieldFromReportMetadata(reportMetadataToUse, fieldName); if (fieldValue) { @@ -644,7 +658,7 @@ export default function ProcessInstanceListTable({ return null; } - let newReportMetadata = null; + let newReportMetadata: ReportMetadata | null = null; if (reportMetadata) { newReportMetadata = { ...reportMetadata }; } @@ -677,41 +691,44 @@ export default function ProcessInstanceListTable({ endToSeconds ); - if (processStatusSelection.length > 0) { - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - 'process_status', - processStatusSelection.join(',') - ); - } else { - removeFieldFromReportMetadata(newReportMetadata, 'process_status'); - } + insertOrUpdateFieldInReportMetadata( + newReportMetadata, + 'process_status', + processStatusSelection.length > 0 + ? processStatusSelection.join(',') + : null + ); + insertOrUpdateFieldInReportMetadata( + newReportMetadata, + 'process_model_identifier', + processModelSelection ? processModelSelection.id : null + ); + insertOrUpdateFieldInReportMetadata( + newReportMetadata, + 'process_initiator_username', + processInitiatorSelection + ); - if (processModelSelection) { - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - 'process_model_identifier', - processModelSelection.id - ); - } else { - removeFieldFromReportMetadata( - newReportMetadata, - 'process_model_identifier' - ); - } + insertOrUpdateFieldInReportMetadata( + newReportMetadata, + 'with_oldest_open_task', + withOldestOpenTask + ); + insertOrUpdateFieldInReportMetadata( + newReportMetadata, + 'user_group_identifier', + selectedUserGroup + ); + systemReportOptions.forEach((systemReportOption: string) => { + if (newReportMetadata) { + insertOrUpdateFieldInReportMetadata( + newReportMetadata, + systemReportOption, + systemReport === systemReportOption + ); + } + }); - if (processInitiatorSelection) { - insertOrUpdateFieldInReportMetadata( - newReportMetadata, - 'process_initiator_username', - processInitiatorSelection - ); - } else { - removeFieldFromReportMetadata( - newReportMetadata, - 'process_initiator_username' - ); - } return newReportMetadata; }; @@ -843,14 +860,19 @@ export default function ProcessInstanceListTable({ clearFilters(); const selectedReport = selection.selectedItem; setProcessInstanceReportSelection(selectedReport); + removeError(); + setProcessInstanceReportJustSaved(mode || null); + setListHasBeenFiltered(false); let queryParamString = ''; if (selectedReport) { queryParamString = `?report_id=${selectedReport.id}`; - } - removeError(); - setProcessInstanceReportJustSaved(mode || null); + HttpService.makeCallToBackend({ + path: `/process-instances/report-metadata${queryParamString}`, + successCallback: getProcessInstances, + }); + } navigate(`${processInstanceListPathPrefix}${queryParamString}`); }; @@ -1181,6 +1203,75 @@ export default function ProcessInstanceListTable({ return null; }; + const handleAdvancedOptionsClose = () => { + setShowAdvancedOptions(false); + }; + + const advancedOptionsModal = () => { + if (!showAdvancedOptions) { + return null; + } + const formElements = ( + <> + + + item} + selectedItem={systemReport} + onChange={(value: any) => { + setSystemReport(value.selectedItem); + setRequiresRefilter(true); + }} + /> + + + item} + selectedItem={selectedUserGroup} + onChange={(value: any) => { + setSelectedUserGroup(value.selectedItem); + setRequiresRefilter(true); + }} + /> + + +
+ + + { + setWithOldestOpenTask(value.target.checked); + setRequiresRefilter(true); + }} + /> + + +
+ + ); + return ( + + {formElements} + + ); + }; + const filterOptions = () => { if (!showFilterOptions) { return null; @@ -1366,10 +1457,20 @@ export default function ProcessInstanceListTable({ - + {saveAsReportComponent()} {deleteReportComponent()} + + + ); @@ -1567,12 +1668,39 @@ export default function ProcessInstanceListTable({ return null; }; - const linkToReport = () => { - if (!showLinkToReport) { + const tableTitleLine = () => { + if (!showLinkToReport && !headerElement) { return null; } + let filterButtonLink = null; + if (showLinkToReport) { + filterButtonLink = ( + +