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

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 

31 

32 

33class TableSerializer(serializers.ListSerializer): 

34 child = serializers.DictField() 

35 

36 

37class ResultGraphIndexedItemSerializer(serializers.Serializer): 

38 id = serializers.IntegerField() 

39 key = serializers.CharField(allow_null=True) 

40 label = serializers.CharField() 

41 type = serializers.CharField() 

42 

43 

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) 

52 

53 

54class ResultGraphResponseSerializer(serializers.ListSerializer): 

55 child = serializers.ListField(child=ResultGraphPointSerializer()) 

56 

57 

58class DataRowViewSet(ModelViewSet): 

59 serializer_class = DataRowSerializer 

60 pagination_class = ViewSetPagination 

61 

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 

68 

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) 

77 

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") 

88 

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) 

91 

92 try: 

93 scenario = Scenario.objects.get(id=scenario_id) 

94 except Scenario.DoesNotExist: 

95 return Response([], status=404) 

96 

97 is_dynamic = scenario.enable_dynamics 

98 

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([]) 

104 

105 row_ids = [r.id for r in paginated_rows] 

106 row_index_map = {r.id: r.index for r in paginated_rows} 

107 

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 ) 

114 

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 

124 

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) 

135 

136 response = self.get_paginated_response(TableSerializer(data).data) 

137 

138 return response 

139 

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") 

161 

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) 

164 

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) 

176 

177 return Response(table.model_dump(mode="json")) 

178 

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") 

197 

198 try: 

199 scenario = Scenario.objects.get(id=scenario_id) 

200 except Scenario.DoesNotExist: 

201 return Response([], status=404) 

202 

203 is_dynamic = scenario.enable_dynamics 

204 

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) 

207 

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 ) 

217 

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 

241 

242 if key not in prop_key_mapping: 

243 prop_key_mapping[key] = [] 

244 

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()) 

254 

255 return Response(TableSerializer(output_solutions, many=True).data) 

256 

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") 

285 

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) 

288 

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) 

307 

308 return Response( 

309 ResultSummariesResponse(summaries).model_dump(mode="json") 

310 )