]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Performance: Increases workflow related M2M prefetching (#12618)
authorTrenton H <797416+stumpylog@users.noreply.github.com>
Tue, 21 Apr 2026 22:01:51 +0000 (15:01 -0700)
committerGitHub <noreply@github.com>
Tue, 21 Apr 2026 22:01:51 +0000 (22:01 +0000)
src/documents/serialisers.py
src/documents/tests/test_api_workflows.py
src/documents/views.py

index 3cba0fafaa0b824988b84dcd622c34de93a54505..e3037eeae369fbce8aaf82f81f786618b58e56bd 100644 (file)
@@ -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(
index d23a2dc4798df731e7e167770e0edbb5acdc9d8e..a1942d746c379a9778744ff8cc74bc77cb2ee38e 100644 (file)
@@ -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:
index a96c24502cd4d3e62ac543a54628416474970c97..789d7a659bff4d8dfbf7dacc882b0bf86f44cd80 100644 (file)
@@ -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",
+                ),
+            ),
         )
     )