Coverage for backend/django/core/auxiliary/viewsets/DataRowViewSet.py: 85%
150 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
1from core.viewset import ModelViewSet
2from rest_framework.response import Response
3from core.auxiliary.models.DataCell import DataCell
4from core.auxiliary.models.DataRow import DataRow
5from core.auxiliary.models.Solution import Solution
6from core.auxiliary.models.Scenario import Scenario
7from core.auxiliary.serializers.DataRowSerializer import DataRowSerializer
8from core.auxiliary.services.result_summary.contracts import (
9 ResultSummaryRequest,
10 ResultSummariesResponse,
11)
12from core.auxiliary.services.result_summary.cache import (
13 ResultSummaryCacheUnavailable,
14 get_result_summaries_with_cache,
15)
16from common.models.notifications.payloads import ResultSummaryStatusPayload
17from core.auxiliary.services.result_object_table import (
18 ResultObjectTableRequest,
19 ResultObjectTableResponse,
20 get_result_object_table,
21)
22from core.auxiliary.services.result_series_identity import (
23 result_series_identity_payload,
24)
25from core.auxiliary.services.scenario_time_series import get_mss_time_value
26from pydantic import ValidationError
27from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
28from rest_framework.decorators import action
29from rest_framework import serializers
30from core.pagination import ViewSetPagination
33class TableSerializer(serializers.ListSerializer):
34 child = serializers.DictField()
37class ResultGraphIndexedItemSerializer(serializers.Serializer):
38 id = serializers.IntegerField()
39 key = serializers.CharField(allow_null=True)
40 label = serializers.CharField()
41 type = serializers.CharField()
44class ResultGraphPointSerializer(serializers.Serializer):
45 index = serializers.FloatField()
46 key = serializers.CharField()
47 label = serializers.CharField()
48 unit = serializers.CharField(allow_null=True)
49 property_value_id = serializers.IntegerField()
50 indexed_items = ResultGraphIndexedItemSerializer(many=True)
51 value = serializers.FloatField(allow_null=True)
54class ResultGraphResponseSerializer(serializers.ListSerializer):
55 child = serializers.ListField(child=ResultGraphPointSerializer())
58class DataRowViewSet(ModelViewSet):
59 serializer_class = DataRowSerializer
60 pagination_class = ViewSetPagination
62 def get_queryset(self):
63 queryset = DataRow.objects.all()
64 scenario = self.request.query_params.get("scenario")
65 if scenario is not None: 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true
66 queryset = queryset.filter(scenario_id=scenario)
67 return queryset
69 @extend_schema(
70 parameters=[
71 OpenApiParameter(name="scenario",
72 required=True, type=OpenApiTypes.INT),
73 ]
74 )
75 def list(self, request):
76 return super().list(request)
78 @extend_schema(
79 parameters=[
80 OpenApiParameter(name="scenario", required=True,
81 type=OpenApiTypes.INT),
82 ],
83 responses=TableSerializer()
84 )
85 @action(detail=False, methods=['get'], url_path='full-table')
86 def full_input_table(self, request):
87 scenario_id = request.query_params.get("scenario")
89 if not scenario_id: 89 ↛ 90line 89 didn't jump to line 90 because the condition on line 89 was never true
90 return Response([], status=400)
92 try:
93 scenario = Scenario.objects.get(id=scenario_id)
94 except Scenario.DoesNotExist:
95 return Response([], status=404)
97 is_dynamic = scenario.enable_dynamics
99 rows_qs = DataRow.objects.filter(
100 scenario=scenario).order_by("index")
101 paginated_rows = self.paginate_queryset(rows_qs)
102 if not paginated_rows: 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true
103 return self.get_paginated_response([])
105 row_ids = [r.id for r in paginated_rows]
106 row_index_map = {r.id: r.index for r in paginated_rows}
108 # 2️⃣ Get all DataCells for those rows in ONE query
109 data_cells_qs = (
110 DataCell.objects
111 .filter(data_row_id__in=row_ids)
112 .select_related("data_column") # avoid N+1 lookups
113 )
115 # 3️⃣ Build a set of all columns (distinct column names)
116 columns = sorted({dc.data_column.name for dc in data_cells_qs})
117 # 4️⃣ Pre-group DataCells in memory by row_id
118 values_by_row = {}
119 for dc in data_cells_qs:
120 idx = row_index_map[dc.data_row_id]
121 if idx not in values_by_row: 121 ↛ 123line 121 didn't jump to line 123 because the condition on line 121 was always true
122 values_by_row[idx] = {}
123 values_by_row[idx][dc.data_column.name] = dc.value
125 # 5️⃣ Build the table data
126 data = []
127 for idx in sorted(row_index_map.values()):
128 if is_dynamic: 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true
129 row_data = {"Time step": idx}
130 else:
131 row_data = {"Row": idx}
132 for col in columns:
133 row_data[col] = values_by_row.get(idx, {}).get(col)
134 data.append(row_data)
136 response = self.get_paginated_response(TableSerializer(data).data)
138 return response
140 @extend_schema(
141 parameters=[
142 OpenApiParameter(name="scenario", required=True,
143 type=OpenApiTypes.INT),
144 OpenApiParameter(name="simulation_object",
145 required=True, type=OpenApiTypes.INT),
146 OpenApiParameter(name="page", required=False,
147 type=OpenApiTypes.INT),
148 ],
149 responses=ResultObjectTableResponse,
150 )
151 @action(
152 detail=False,
153 methods=['get'],
154 url_path='object-table',
155 pagination_class=None,
156 )
157 def object_table(self, request):
158 scenario_id = request.query_params.get("scenario")
159 simulation_object = request.query_params.get("simulation_object")
160 page = request.query_params.get("page")
162 if not scenario_id or not simulation_object: 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true
163 return Response([], status=400)
165 try:
166 object_table_request = ResultObjectTableRequest(
167 scenario_id=scenario_id,
168 simulation_object_id=simulation_object,
169 page=page or 1,
170 )
171 table = get_result_object_table(object_table_request)
172 except ValidationError as error:
173 return Response(error.errors(include_context=False), status=400)
174 except Scenario.DoesNotExist:
175 return Response([], status=404)
177 return Response(table.model_dump(mode="json"))
179 @extend_schema(
180 parameters=[
181 OpenApiParameter(name="scenario", required=True,
182 type=OpenApiTypes.INT),
183 OpenApiParameter(name="simulation_object",
184 required=True, type=OpenApiTypes.INT),
185 ],
186 responses=ResultGraphResponseSerializer()
187 )
188 @action(
189 detail=False,
190 methods=['get'],
191 url_path='output-graph',
192 pagination_class=None,
193 )
194 def output_graph(self, request):
195 scenario_id = request.query_params.get("scenario")
196 simulation_object = request.query_params.get("simulation_object")
198 try:
199 scenario = Scenario.objects.get(id=scenario_id)
200 except Scenario.DoesNotExist:
201 return Response([], status=404)
203 is_dynamic = scenario.enable_dynamics
205 if not scenario_id or not simulation_object: 205 ↛ 206line 205 didn't jump to line 206 because the condition on line 205 was never true
206 return Response([], status=400)
208 solutions = (
209 Solution.objects.filter(
210 scenario_id=scenario_id,
211 property__property__set__simulationObject_id=simulation_object,
212 )
213 .select_related("property", "property__property")
214 .prefetch_related("property__indexedItems")
215 .order_by("solve_index")
216 )
218 output_solutions = []
219 if is_dynamic:
220 for sol in solutions:
221 property_value = sol.property
222 identity = result_series_identity_payload(property_value)
223 identity_payload = identity.model_dump(mode="json")
224 arr = []
225 for i, value in enumerate(sol.values):
226 arr.append(
227 {
228 "index": i,
229 **identity_payload,
230 "value": value,
231 }
232 )
233 output_solutions.append(arr)
234 else:
235 prop_key_mapping = {}
236 for sol in solutions:
237 property_value = sol.property
238 identity = result_series_identity_payload(property_value)
239 identity_payload = identity.model_dump(mode="json")
240 key = identity.key
242 if key not in prop_key_mapping:
243 prop_key_mapping[key] = []
245 prop_key_mapping[key].append(
246 {
247 "index": get_mss_time_value(scenario, sol.solve_index),
248 **identity_payload,
249 # For MSS, values has only one value.
250 "value": sol.values[0],
251 }
252 )
253 output_solutions = list(prop_key_mapping.values())
255 return Response(TableSerializer(output_solutions, many=True).data)
257 @extend_schema(
258 parameters=[
259 OpenApiParameter(name="scenario", required=True,
260 type=OpenApiTypes.INT),
261 OpenApiParameter(name="simulation_object",
262 required=True, type=OpenApiTypes.INT),
263 OpenApiParameter(
264 name="histogram_bin_counts",
265 required=False,
266 type=OpenApiTypes.STR,
267 description=(
268 "JSON array of per-result histogram bin overrides. "
269 "Each item must include key and histogram_bins "
270 "(auto, 10, 20, 30, or 50)."
271 ),
272 ),
273 ],
274 responses={
275 200: ResultSummariesResponse,
276 202: ResultSummaryStatusPayload,
277 409: ResultSummaryStatusPayload,
278 },
279 )
280 @action(detail=False, methods=['get'], url_path='output-summary', pagination_class=None)
281 def output_summary(self, request):
282 scenario_id = request.query_params.get("scenario")
283 simulation_object = request.query_params.get("simulation_object")
284 histogram_bin_counts = request.query_params.get("histogram_bin_counts")
286 if not scenario_id or not simulation_object: 286 ↛ 287line 286 didn't jump to line 287 because the condition on line 286 was never true
287 return Response([], status=400)
289 try:
290 summary_request = ResultSummaryRequest(
291 scenario_id=scenario_id,
292 simulation_object_id=simulation_object,
293 histogram_bin_counts=histogram_bin_counts,
294 )
295 summaries = get_result_summaries_with_cache(
296 summary_request,
297 )
298 except ResultSummaryCacheUnavailable as unavailable:
299 return Response(
300 unavailable.payload.model_dump(mode="json"),
301 status=unavailable.response_status,
302 )
303 except ValidationError as error:
304 return Response(error.errors(include_context=False), status=400)
305 except Scenario.DoesNotExist:
306 return Response([], status=404)
308 return Response(
309 ResultSummariesResponse(summaries).model_dump(mode="json")
310 )