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