Skip to content

Commit a5c7fac

Browse files
authored
Improve automation history performance. (baserow#5397)
1 parent 1331341 commit a5c7fac

23 files changed

Lines changed: 1179 additions & 261 deletions

File tree

backend/src/baserow/contrib/automation/api/history/__init__.py

Whitespace-only changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from rest_framework.status import HTTP_404_NOT_FOUND
2+
3+
ERROR_AUTOMATION_WORKFLOW_HISTORY_DOES_NOT_EXIST = (
4+
"ERROR_AUTOMATION_WORKFLOW_HISTORY_DOES_NOT_EXIST",
5+
HTTP_404_NOT_FOUND,
6+
"The automation workflow history does not exist.",
7+
)
8+
9+
ERROR_AUTOMATION_NODE_HISTORY_DOES_NOT_EXIST = (
10+
"ERROR_AUTOMATION_NODE_HISTORY_DOES_NOT_EXIST",
11+
HTTP_404_NOT_FOUND,
12+
"The automation node history does not exist.",
13+
)
14+
15+
ERROR_AUTOMATION_NODE_RESULT_DOES_NOT_EXIST = (
16+
"ERROR_AUTOMATION_NODE_RESULT_DOES_NOT_EXIST",
17+
HTTP_404_NOT_FOUND,
18+
"The automation node result does not exist.",
19+
)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from drf_spectacular.types import OpenApiTypes
2+
from drf_spectacular.utils import extend_schema_field
3+
from rest_framework import serializers
4+
5+
from baserow.contrib.automation.history.models import (
6+
AutomationNodeHistory,
7+
AutomationNodeResult,
8+
)
9+
10+
11+
class AutomationNodeHistorySerializer(serializers.ModelSerializer):
12+
node_type = serializers.SerializerMethodField()
13+
node_label = serializers.SerializerMethodField()
14+
parent_node_id = serializers.SerializerMethodField()
15+
iteration = serializers.SerializerMethodField()
16+
iteration_path = serializers.SerializerMethodField()
17+
edge_label = serializers.SerializerMethodField()
18+
19+
class Meta:
20+
model = AutomationNodeHistory
21+
fields = (
22+
"id",
23+
"started_on",
24+
"completed_on",
25+
"message",
26+
"status",
27+
"workflow_history",
28+
"node",
29+
"node_type",
30+
"node_label",
31+
"parent_node_id",
32+
"iteration",
33+
"iteration_path",
34+
"edge_label",
35+
)
36+
37+
@extend_schema_field(OpenApiTypes.STR)
38+
def get_node_type(self, obj):
39+
return obj.node.get_type().type
40+
41+
@extend_schema_field(OpenApiTypes.STR)
42+
def get_node_label(self, obj):
43+
return obj.node.label
44+
45+
@extend_schema_field(OpenApiTypes.INT)
46+
def get_parent_node_id(self, obj):
47+
parent_nodes = obj.node.get_parent_nodes()
48+
if not parent_nodes:
49+
return None
50+
return parent_nodes[-1].id
51+
52+
def _get_first_node_result(self, obj):
53+
results = obj.node_results.all()
54+
return results[0] if results else None
55+
56+
@extend_schema_field(OpenApiTypes.INT)
57+
def get_iteration(self, obj):
58+
result = self._get_first_node_result(obj)
59+
if result is None:
60+
return None
61+
if result.iteration_path:
62+
return int(result.iteration_path.rsplit(".", 1)[-1])
63+
return 0
64+
65+
@extend_schema_field(OpenApiTypes.STR)
66+
def get_iteration_path(self, obj):
67+
result = self._get_first_node_result(obj)
68+
return result.iteration_path if result else ""
69+
70+
@extend_schema_field(OpenApiTypes.STR)
71+
def get_edge_label(self, obj):
72+
return self.context.get("edge_labels", {}).get(obj.id, "")
73+
74+
75+
class AutomationNodeResultSerializer(serializers.ModelSerializer):
76+
class Meta:
77+
model = AutomationNodeResult
78+
fields = ("result",)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from django.urls import re_path
2+
3+
from baserow.contrib.automation.api.history.views import (
4+
AutomationNodeHistoriesView,
5+
AutomationNodeResultView,
6+
)
7+
8+
app_name = "baserow.contrib.automation.api.history"
9+
10+
urlpatterns = [
11+
re_path(
12+
r"workflow_histories/(?P<workflow_history_id>[0-9]+)/node_histories/$",
13+
AutomationNodeHistoriesView.as_view(),
14+
name="node_histories",
15+
),
16+
re_path(
17+
r"node_histories/(?P<node_history_id>[0-9]+)/result/$",
18+
AutomationNodeResultView.as_view(),
19+
name="node_result",
20+
),
21+
]
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from drf_spectacular.types import OpenApiTypes
2+
from drf_spectacular.utils import OpenApiParameter, extend_schema
3+
from rest_framework.permissions import IsAuthenticated
4+
from rest_framework.response import Response
5+
from rest_framework.views import APIView
6+
7+
from baserow.api.decorators import map_exceptions
8+
from baserow.api.schemas import CLIENT_SESSION_ID_SCHEMA_PARAMETER, get_error_schema
9+
from baserow.contrib.automation.api.history.errors import (
10+
ERROR_AUTOMATION_NODE_HISTORY_DOES_NOT_EXIST,
11+
ERROR_AUTOMATION_NODE_RESULT_DOES_NOT_EXIST,
12+
ERROR_AUTOMATION_WORKFLOW_HISTORY_DOES_NOT_EXIST,
13+
)
14+
from baserow.contrib.automation.api.history.serializers import (
15+
AutomationNodeHistorySerializer,
16+
AutomationNodeResultSerializer,
17+
)
18+
from baserow.contrib.automation.history.exceptions import (
19+
AutomationNodeHistoryDoesNotExist,
20+
AutomationWorkflowHistoryDoesNotExist,
21+
AutomationWorkflowHistoryNodeResultDoesNotExist,
22+
)
23+
from baserow.contrib.automation.history.service import AutomationHistoryService
24+
25+
AUTOMATION_HISTORY_TAG = "Automation history"
26+
27+
28+
class AutomationNodeHistoriesView(APIView):
29+
permission_classes = (IsAuthenticated,)
30+
31+
@extend_schema(
32+
parameters=[
33+
OpenApiParameter(
34+
name="workflow_history_id",
35+
location=OpenApiParameter.PATH,
36+
type=OpenApiTypes.INT,
37+
description="The id of the workflow history.",
38+
),
39+
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
40+
],
41+
tags=[AUTOMATION_HISTORY_TAG],
42+
operation_id="get_automation_node_histories",
43+
description="Returns all node histories for the given workflow history.",
44+
responses={
45+
200: AutomationNodeHistorySerializer(many=True),
46+
404: get_error_schema(["ERROR_AUTOMATION_WORKFLOW_HISTORY_DOES_NOT_EXIST"]),
47+
},
48+
)
49+
@map_exceptions(
50+
{
51+
AutomationWorkflowHistoryDoesNotExist: (
52+
ERROR_AUTOMATION_WORKFLOW_HISTORY_DOES_NOT_EXIST
53+
),
54+
}
55+
)
56+
def get(self, request, workflow_history_id: int):
57+
service = AutomationHistoryService()
58+
node_histories = service.get_node_histories(request.user, workflow_history_id)
59+
edge_labels = service.get_edge_labels(request.user, node_histories)
60+
serializer = AutomationNodeHistorySerializer(
61+
node_histories,
62+
many=True,
63+
context={"edge_labels": edge_labels},
64+
)
65+
return Response(serializer.data)
66+
67+
68+
class AutomationNodeResultView(APIView):
69+
permission_classes = (IsAuthenticated,)
70+
71+
@extend_schema(
72+
parameters=[
73+
OpenApiParameter(
74+
name="node_history_id",
75+
location=OpenApiParameter.PATH,
76+
type=OpenApiTypes.INT,
77+
description="The id of the node history.",
78+
),
79+
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
80+
],
81+
tags=[AUTOMATION_HISTORY_TAG],
82+
operation_id="get_automation_node_result",
83+
description="Returns the node history's result JSON.",
84+
responses={
85+
200: AutomationNodeResultSerializer,
86+
404: get_error_schema(
87+
[
88+
"ERROR_AUTOMATION_NODE_HISTORY_DOES_NOT_EXIST",
89+
"ERROR_AUTOMATION_NODE_RESULT_DOES_NOT_EXIST",
90+
]
91+
),
92+
},
93+
)
94+
@map_exceptions(
95+
{
96+
AutomationNodeHistoryDoesNotExist: (
97+
ERROR_AUTOMATION_NODE_HISTORY_DOES_NOT_EXIST
98+
),
99+
AutomationWorkflowHistoryNodeResultDoesNotExist: (
100+
ERROR_AUTOMATION_NODE_RESULT_DOES_NOT_EXIST
101+
),
102+
}
103+
)
104+
def get(self, request, node_history_id: int):
105+
node_result = AutomationHistoryService().get_node_history_result(
106+
request.user, node_history_id
107+
)
108+
serializer = AutomationNodeResultSerializer(node_result)
109+
return Response(serializer.data)

backend/src/baserow/contrib/automation/api/urls.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.urls import include, path, re_path
22

3+
from baserow.contrib.automation.api.history import urls as history_urls
34
from baserow.contrib.automation.api.nodes import urls as node_urls
45
from baserow.contrib.automation.api.workflows import urls as workflow_urls
56

@@ -30,6 +31,13 @@
3031
namespace="nodes",
3132
),
3233
),
34+
path(
35+
"",
36+
include(
37+
history_urls,
38+
namespace="history",
39+
),
40+
),
3341
]
3442

3543
urlpatterns = [

backend/src/baserow/contrib/automation/api/workflows/serializers.py

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from baserow.api.pagination import PageNumberPagination
66
from baserow.contrib.automation.models import (
77
AutomationHistory,
8-
AutomationNodeHistory,
98
AutomationWorkflow,
109
AutomationWorkflowHistory,
1110
)
@@ -118,70 +117,12 @@ class Meta:
118117
)
119118

120119

121-
class AutomationNodeHistorySerializer(AutomationHistorySerializer):
122-
parent_node_id = serializers.SerializerMethodField()
123-
iteration = serializers.SerializerMethodField()
124-
result = serializers.SerializerMethodField()
125-
node_type = serializers.SerializerMethodField()
126-
node_label = serializers.SerializerMethodField()
127-
128-
class Meta:
129-
model = AutomationNodeHistory
130-
fields = AutomationHistorySerializer.Meta.fields + (
131-
"workflow_history",
132-
"node",
133-
"node_type",
134-
"node_label",
135-
"parent_node_id",
136-
"iteration",
137-
"result",
138-
)
139-
140-
def _get_first_node_result(self, obj):
141-
results = obj.node_results.all()
142-
return results[0] if results else None
143-
144-
@extend_schema_field(OpenApiTypes.STR)
145-
def get_node_type(self, obj):
146-
return obj.node.get_type().type
147-
148-
@extend_schema_field(OpenApiTypes.STR)
149-
def get_node_label(self, obj):
150-
return obj.node.label
151-
152-
@extend_schema_field(OpenApiTypes.INT)
153-
def get_parent_node_id(self, obj):
154-
parent_nodes = obj.node.get_parent_nodes()
155-
if not parent_nodes:
156-
return None
157-
return parent_nodes[-1].id
158-
159-
@extend_schema_field(OpenApiTypes.INT)
160-
def get_iteration(self, obj):
161-
result = self._get_first_node_result(obj)
162-
if result is None:
163-
return None
164-
165-
if result.iteration_path:
166-
return int(result.iteration_path.rsplit(".", 1)[-1])
167-
168-
return 0
169-
170-
def get_result(self, obj):
171-
result = self._get_first_node_result(obj)
172-
return result.result if result else {}
173-
174-
175120
class AutomationWorkflowHistorySerializer(AutomationHistorySerializer):
176-
node_histories = AutomationNodeHistorySerializer(read_only=True, many=True)
177-
178121
class Meta:
179122
model = AutomationWorkflowHistory
180123
fields = AutomationHistorySerializer.Meta.fields + (
181124
"is_test_run",
182-
"event_payload",
183125
"simulate_until_node",
184-
"node_histories",
185126
)
186127

187128

backend/src/baserow/contrib/automation/history/exceptions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,15 @@ def __init__(self, history_id=None, *args, **kwargs):
1919

2020
class AutomationWorkflowHistoryNodeResultDoesNotExist(AutomationWorkflowHistoryError):
2121
"""When the result entry doesn't exist for the given node/history."""
22+
23+
24+
class AutomationNodeHistoryDoesNotExist(AutomationWorkflowHistoryError):
25+
"""When the node history entry doesn't exist."""
26+
27+
def __init__(self, node_history_id=None, *args, **kwargs):
28+
self.node_history_id = node_history_id
29+
super().__init__(
30+
f"The automation node history {node_history_id} does not exist.",
31+
*args,
32+
**kwargs,
33+
)

0 commit comments

Comments
 (0)