From 89a9e7f1908a37da01fa36bf1ffdab5ba9b6ea97 Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:01:51 -0700 Subject: [PATCH] Performance: Increases workflow related M2M prefetching (#12618) --- src/documents/serialisers.py | 22 ++++--------- src/documents/tests/test_api_workflows.py | 34 +++++++++++++++++++ src/documents/views.py | 40 +++++++++++++++++++++-- 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 3cba0fafa..e3037eeae 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -3352,13 +3352,13 @@ class WorkflowSerializer(serializers.ModelSerializer[Workflow]): ManyToMany fields dont support e.g. on_delete so we need to discard unattached triggers and actions manually """ - for trigger in WorkflowTrigger.objects.all(): - if trigger.workflows.all().count() == 0: - trigger.delete() + WorkflowTrigger.objects.annotate( + workflow_count=Count("workflows"), + ).filter(workflow_count=0).delete() - for action in WorkflowAction.objects.all(): - if action.workflows.all().count() == 0: - action.delete() + WorkflowAction.objects.annotate( + workflow_count=Count("workflows"), + ).filter(workflow_count=0).delete() WorkflowActionEmail.objects.filter(action=None).delete() WorkflowActionWebhook.objects.filter(action=None).delete() @@ -3387,16 +3387,6 @@ class WorkflowSerializer(serializers.ModelSerializer[Workflow]): return instance - def to_representation(self, instance: Workflow) -> dict[str, Any]: - data = super().to_representation(instance) - actions = instance.actions.order_by("order", "pk") - data["actions"] = WorkflowActionSerializer( - actions, - many=True, - context=self.context, - ).data - return data - class TrashSerializer(SerializerWithPerms): documents = serializers.ListField( diff --git a/src/documents/tests/test_api_workflows.py b/src/documents/tests/test_api_workflows.py index d23a2dc47..a1942d746 100644 --- a/src/documents/tests/test_api_workflows.py +++ b/src/documents/tests/test_api_workflows.py @@ -99,6 +99,40 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): self.action.assign_correspondent.pk, ) + def test_api_get_workflow_actions_ordered(self) -> None: + """ + GIVEN: + - A workflow with two actions added in reverse order (order=1 before order=0) + WHEN: + - API is called to get workflows + THEN: + - Actions are returned sorted by order ascending + """ + # Created before action_first so its pk is lower — ensures pk order + # disagrees with the order field, catching regressions if order_by is removed. + action_second = WorkflowAction.objects.create( + assign_title="Second action", + order=1, + ) + action_first = WorkflowAction.objects.create( + assign_title="First action", + order=0, + ) + self.workflow.actions.add(action_second) + self.workflow.actions.add(action_first) + + response = self.client.get(self.ENDPOINT, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + resp_actions = response.data["results"][0]["actions"] + action_ids = [a["id"] for a in resp_actions] + self.assertIn(action_first.id, action_ids) + self.assertIn(action_second.id, action_ids) + self.assertLess( + action_ids.index(action_first.id), + action_ids.index(action_second.id), + ) + def test_api_create_workflow(self) -> None: """ GIVEN: diff --git a/src/documents/views.py b/src/documents/views.py index a96c24502..789d7a659 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -4485,8 +4485,44 @@ class WorkflowViewSet(ModelViewSet[Workflow]): Workflow.objects.all() .order_by("order") .prefetch_related( - "triggers", - "actions", + Prefetch( + "triggers", + queryset=WorkflowTrigger.objects.prefetch_related( + "filter_has_tags", + "filter_has_all_tags", + "filter_has_not_tags", + "filter_has_any_correspondents", + "filter_has_not_correspondents", + "filter_has_any_document_types", + "filter_has_not_document_types", + "filter_has_any_storage_paths", + "filter_has_not_storage_paths", + ), + ), + Prefetch( + "actions", + queryset=WorkflowAction.objects.order_by( + "order", + "pk", + ).prefetch_related( + "assign_tags", + "assign_view_users", + "assign_view_groups", + "assign_change_users", + "assign_change_groups", + "assign_custom_fields", + "remove_tags", + "remove_correspondents", + "remove_document_types", + "remove_storage_paths", + "remove_custom_fields", + "remove_owners", + "remove_view_users", + "remove_view_groups", + "remove_change_users", + "remove_change_groups", + ), + ), ) )