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 3c0e8646d..ad29dd065 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py @@ -75,6 +75,10 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): created_at_in_seconds = db.Column(db.Integer) updated_at_in_seconds = db.Column(db.Integer) + @classmethod + def default_order_by(cls) -> list[str]: + return ["-start_in_seconds", "-id"] + @classmethod def add_fixtures(cls) -> None: """Add_fixtures.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index d211f1682..d9a7f68a3 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -3,6 +3,7 @@ import json import random import string import uuid +import re from typing import Any from typing import Dict from typing import Optional @@ -944,6 +945,7 @@ def process_instance_list( UserGroupAssignmentModel.user_id == g.user.id ) + instance_metadata_aliases = {} stock_columns = ProcessInstanceReportService.get_column_names_for_model( ProcessInstanceModel ) @@ -951,15 +953,18 @@ def process_instance_list( if column["accessor"] in stock_columns: continue instance_metadata_alias = aliased(ProcessInstanceMetadataModel) + instance_metadata_aliases[column['accessor']] = instance_metadata_alias - filter_for_column = next( - ( - f - for f in process_instance_report.report_metadata["filter_by"] - if f["field_name"] == column["accessor"] - ), - None, - ) + 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, @@ -974,11 +979,28 @@ def process_instance_list( 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(instance_metadata_aliases[attribute].value.desc()) + else: + order_by_query_array.append(instance_metadata_aliases[attribute].value.asc()) + process_instances = ( process_instance_query.group_by(ProcessInstanceModel.id) .add_columns(ProcessInstanceModel.id) .order_by( - ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore + *order_by_query_array ) .paginate(page=page, per_page=per_page, error_out=False) ) diff --git a/spiffworkflow-backend/tests/data/nested-task-data-structure/nested-task-data-structure.bpmn b/spiffworkflow-backend/tests/data/nested-task-data-structure/nested-task-data-structure.bpmn index 4588bef0f..7452216a6 100644 --- a/spiffworkflow-backend/tests/data/nested-task-data-structure/nested-task-data-structure.bpmn +++ b/spiffworkflow-backend/tests/data/nested-task-data-structure/nested-task-data-structure.bpmn @@ -14,7 +14,8 @@ Flow_18gs4jt outer = {} invoice_number = 123 -outer["inner"] = 'sweet1' +outer["inner"] = 'sweet1' +outer['time'] = time.time_ns() 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 a17a38755..05e0977a6 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -2657,3 +2657,86 @@ class TestProcessApi(BaseTest): {"Header": "key2", "accessor": "key2", "filterable": True}, {"Header": "key3", "accessor": "key3", "filterable": True}, ] + + def test_process_instance_list_can_order_by_metadata( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + self.create_process_group( + client, with_super_admin_user, "test_group", "test_group" + ) + process_model = load_test_spec( + "test_group/hello_world", + process_model_source_directory="nested-task-data-structure", + ) + ProcessModelService.update_process_model( + process_model, + { + "metadata_extraction_paths": [ + {"key": "time_ns", "path": "outer.time"}, + ] + }, + ) + + process_instance_one = self.create_process_instance_from_process_model( + process_model + ) + processor = ProcessInstanceProcessor(process_instance_one) + processor.do_engine_steps(save=True) + assert process_instance_one.status == "complete" + process_instance_two = self.create_process_instance_from_process_model( + process_model + ) + processor = ProcessInstanceProcessor(process_instance_two) + processor.do_engine_steps(save=True) + assert process_instance_two.status == "complete" + + report_metadata = { + "columns": [ + {"Header": "id", "accessor": "id"}, + {"Header": "Time", "accessor": "time_ns"}, + ], + "order_by": ["time_ns"], + } + report_one = ProcessInstanceReportModel.create_with_attributes( + 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), + ) + 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 + + report_metadata = { + "columns": [ + {"Header": "id", "accessor": "id"}, + {"Header": "Time", "accessor": "time_ns"}, + ], + "order_by": ["-time_ns"], + } + report_two = ProcessInstanceReportModel.create_with_attributes( + 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), + ) + + 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