Coverage for backend/flowsheetInternals/unitops/viewsets/SimulationObjectViewSet.py: 67%

208 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-11-06 23:27 +0000

1import json 

2 

3from core.auxiliary.models.PropertyValue import PropertyValue 

4from core.viewset import ModelViewSet 

5from rest_framework.response import Response 

6from flowsheetInternals.unitops.viewsets.DuplicateSimulationObject import DuplicateSimulationObject 

7from flowsheetInternals.unitops.models.compound_propogation import update_compounds_on_set 

8from flowsheetInternals.unitops.serializers.SimulationObjectSerializer import SimulationObjectRetrieveSerializer 

9from flowsheetInternals.unitops.models import SimulationObject 

10from flowsheetInternals.unitops.models.delete_factory import DeleteFactory 

11from flowsheetInternals.unitops.serializers import * 

12from core.auxiliary.models.PropertySet import PropertySet 

13from core.auxiliary.models.PropertyInfo import PropertyInfo 

14from flowsheetInternals.unitops.models.summary_table_factory import get_composition_summary_table_data, get_stream_summary_table_data, get_unitops_summary_table_data 

15from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes 

16from rest_framework import serializers 

17from rest_framework.decorators import action 

18from rest_framework import status 

19import traceback 

20from django.db.models import Prefetch 

21from common.config_types import PropertyType as PropertyTypeObj 

22from flowsheetInternals.formula_templates.add_template import add_template 

23from flowsheetInternals.unitops.methods.add_expression import add_expression 

24from flowsheetInternals.graphicData.models.groupingModel import Grouping 

25 

26class UpdateCompoundSerializer(serializers.Serializer): 

27 """Payload schema for updating compound selections on a stream.""" 

28 

29 compounds = serializers.ListField(child=serializers.CharField()) 

30 simulationObject = serializers.IntegerField() 

31 

32class AddPropertyTemplateSerializer(serializers.Serializer): 

33 """Payload schema for adding a property template by key to a simulation object.""" 

34 

35 templateKey = serializers.CharField() 

36 

37class DuplicateSimulationObjectSerializer(serializers.Serializer): 

38 """Payload schema for duplicating one or more simulation objects.""" 

39 

40 objectIDs = serializers.ListField(child=serializers.IntegerField()) 

41 x = serializers.FloatField() 

42 y = serializers.FloatField() 

43 

44class AddPortSerializer(serializers.Serializer): 

45 """Payload schema for attaching a new port to a simulation object.""" 

46 

47 simulationObjectId = serializers.IntegerField() 

48 key = serializers.CharField() 

49 stream = serializers.IntegerField(required=False, allow_null=True) #optional if existing stream isnt passed in  

50 

51class MergeDecisionNodesSerializer(serializers.Serializer): 

52 """Payload schema for merging two decision nodes.""" 

53 

54 decisionNodeActive = serializers.IntegerField(required=True) 

55 decisionNodeOver = serializers.IntegerField(required=True) 

56 

57class RestoreObjectsSerializer(serializers.Serializer): 

58 """Payload schema for restoring previously deleted simulation objects.""" 

59 

60 object_ids = serializers.ListField( 

61 child=serializers.IntegerField(), 

62 help_text="Array of object IDs to restore", 

63 min_length=1 

64 ) 

65 

66 def validate(self, data): 

67 """Ensure we have at least one valid ID""" 

68 if not data.get('object_ids'): 

69 raise serializers.ValidationError("At least one object ID must be provided") 

70 return data 

71 

72class SimulationObjectViewSet(ModelViewSet): 

73 """API endpoints for interacting with flowsheet simulation objects.""" 

74 

75 serializer_class = SimulationObjectSerializer 

76 

77 def get_queryset(self): 

78 """Return simulation objects with related property metadata eagerly loaded.""" 

79 # Prefetch related entities to avoid N+1 queries when serializing. 

80 queryset = SimulationObject.objects.all().select_related( 

81 "properties", 

82 "grouping" 

83 ).prefetch_related( 

84 Prefetch( 

85 "properties__ContainedProperties", 

86 queryset=PropertyInfo.objects 

87 .select_related("recycleConnection") 

88 .prefetch_related( 

89 Prefetch("values", queryset=PropertyValue.objects 

90 .select_related("controlManipulated", "controlSetPoint") 

91 .prefetch_related("indexedItems")) 

92 ) 

93 ), 

94 "connectedPorts__unitOp" 

95 ) 

96 

97 return queryset 

98 

99 def get_serializer_class(self): 

100 """Use the retrieve serializer when properties must be serialized.""" 

101 if self.action == "retrieve": 

102 # for the retrieve action, we want to include the properties 

103 # of the simulation object (so a separate serializer is needed) 

104 return SimulationObjectRetrieveSerializer 

105 return SimulationObjectSerializer 

106 

107 @extend_schema( 

108 parameters=[ 

109 OpenApiParameter(name="flowsheet", required=True, type=OpenApiTypes.INT), 

110 ] 

111 ) 

112 def list(self, request): 

113 """List simulation objects filtered by the provided flowsheet ID.""" 

114 return super().list(request) 

115 

116 def destroy(self, request, *args, **kwargs): 

117 """Delete the simulation object via the shared DeleteFactory.""" 

118 instance = self.get_object() 

119 DeleteFactory.delete_object(instance) 

120 

121 return Response(status=status.HTTP_204_NO_CONTENT) 

122 #maybe do local storage and its request it in here 

123 

124 

125 def get_summary_queryset(self): 

126 queryset = SimulationObject.objects.all().select_related( 

127 "properties", 

128 ).prefetch_related( 

129 Prefetch( 

130 "properties__ContainedProperties", 

131 queryset=PropertyInfo.objects 

132 .prefetch_related( 

133 Prefetch("values", queryset=PropertyValue.objects 

134 .prefetch_related("indexedItems")) 

135 ) 

136 ) 

137 ) 

138 

139 return queryset 

140 

141 @extend_schema( 

142 parameters=[ 

143 OpenApiParameter(name="unit_map", type=OpenApiTypes.STR, required=True), 

144 OpenApiParameter(name="groups", type=OpenApiTypes.ANY, required=True) 

145 ], 

146 responses=serializers.DictField() 

147 ) 

148 @action(detail=False, methods=['get'], url_path='streams-summary') 

149 def summary_table_streams(self, request): 

150 unit_map = self.request.query_params.get("unit_map") 

151 groups = self.request.query_params.get("groups") 

152 

153 results = {} 

154 groups = Grouping.objects.filter(id__in=json.loads(groups)) 

155 for current_group in groups: 

156 query_set = self.get_summary_queryset().filter(graphicObject__in=current_group.graphicObjects.all()) 

157 

158 data = get_stream_summary_table_data(query_set, json.loads(unit_map)) 

159 

160 group_name = current_group.simulationObject.componentName 

161 results[group_name] = data 

162 

163 return Response(results, status=200) 

164 

165 @extend_schema( 

166 parameters=[ 

167 OpenApiParameter(name="unit_map", type=OpenApiTypes.STR, required=True), 

168 OpenApiParameter(name="groups", type=OpenApiTypes.ANY, required=True) 

169 ], 

170 responses=serializers.DictField() 

171 ) 

172 @action(detail=False, methods=['get'], url_path='unitops-summary') 

173 def summary_table_unitops(self, request): 

174 unit_map = self.request.query_params.get("unit_map") 

175 groups = self.request.query_params.get("groups") 

176 

177 results = {} 

178 groups = Grouping.objects.filter(id__in=json.loads(groups)) 

179 for current_group in groups: 

180 query_set = self.get_summary_queryset().filter(graphicObject__in=current_group.graphicObjects.all()) 

181 data = get_unitops_summary_table_data(query_set, json.loads(unit_map)) 

182 

183 group_name = current_group.simulationObject.componentName 

184 results[group_name] = data 

185 

186 return Response(results, status=200) 

187 

188 @extend_schema( 

189 parameters=[ 

190 OpenApiParameter(name="compound_mode", type=OpenApiTypes.STR, required=True), 

191 OpenApiParameter(name="measure_type", type=OpenApiTypes.STR, required=True), 

192 OpenApiParameter(name="groups", type=OpenApiTypes.ANY, required=True) 

193 ], 

194 responses=serializers.DictField() 

195 ) 

196 @action(detail=False, methods=['get'], url_path='compounds-summary') 

197 def summary_table_compounds(self, request): 

198 compound_mode = self.request.query_params.get("compound_mode") 

199 measure_type = self.request.query_params.get("measure_type") 

200 groups = self.request.query_params.get("groups") 

201 

202 results = {} 

203 groups = Grouping.objects.filter(id__in=json.loads(groups)) 

204 for current_group in groups: 

205 query_set = self.get_summary_queryset().filter(graphicObject__in=current_group.graphicObjects.all()) 

206 data = get_composition_summary_table_data(queryset=query_set, target_compound_mode=compound_mode, measure_type=measure_type) 

207 

208 group_name = current_group.simulationObject.componentName 

209 results[group_name] = data 

210 

211 return Response(results, status=200) 

212 

213 @extend_schema(request=UpdateCompoundSerializer, responses=None) 

214 @action(detail=False, methods=['post'], url_path='update-compounds') 

215 def update_compounds(self, request): 

216 """Update the selected compounds for the specified simulation object.""" 

217 try: 

218 serializer = UpdateCompoundSerializer(data=request.data) 

219 serializer.is_valid(raise_exception=True) 

220 validated_data = serializer.validated_data 

221 

222 simulation_object = SimulationObject.objects.get( 

223 pk=validated_data.get("simulationObject") 

224 ) 

225 

226 expected_compounds = validated_data.get("compounds") 

227 update_compounds_on_set(simulation_object, expected_compounds) 

228 

229 return Response({'status': 'success'}, status=200) 

230 

231 except (SimulationObject.DoesNotExist, PropertySet.DoesNotExist) as e: 

232 return Response({'status': 'error', 'message': str(e)}, status=404) 

233 except Exception as e: 

234 print(traceback.format_exc()) 

235 return Response({'status': 'error', 'message': str(e),'traceback': traceback.format_exc()}, status=400) 

236 

237 

238 

239 @extend_schema(request=AddPortSerializer, responses=None) 

240 @action(detail=False, methods=['post'], url_path='add-port') 

241 def add_port(self, request): 

242 """Add a port to a simulation object, optionally reusing an existing stream.""" 

243 try: 

244 serializer = AddPortSerializer(data=request.data) 

245 serializer.is_valid(raise_exception=True) 

246 validated_data = serializer.validated_data 

247 

248 simulation_object = SimulationObject.objects.get( 

249 pk=validated_data.get("simulationObjectId") 

250 ) 

251 

252 key = validated_data.get("key") 

253 

254 streamId = validated_data.get("stream") 

255 existing_stream = None 

256 

257 if streamId is not None: 257 ↛ 258line 257 didn't jump to line 258 because the condition on line 257 was never true

258 existing_stream = SimulationObject.objects.get(pk=streamId) 

259 # Reuse the existing stream to keep compound propagation in sync. 

260 simulation_object.add_port(key, existing_stream) 

261 else: 

262 simulation_object.add_port(key) 

263 

264 return Response({'status': 'success'}, status=200) 

265 

266 except (SimulationObject.DoesNotExist, PropertySet.DoesNotExist) as e: 

267 return Response({'status': 'error', 'message': str(e)}, status=404) 

268 except Exception as e: 

269 return Response({'status': 'error', 'message': str(e),'traceback': traceback.format_exc()}, status=400) 

270 

271 

272 

273 @extend_schema(request=MergeDecisionNodesSerializer, responses=None) 

274 @action(detail=False, methods=['post'], url_path='merge-decision-nodes') 

275 def merge_decision_nodes(self, request): 

276 try: 

277 serializer = MergeDecisionNodesSerializer(data=request.data) 

278 serializer.is_valid(raise_exception=True) 

279 validated_data = serializer.validated_data 

280 

281 decision_node_active = SimulationObject.objects.get( 

282 pk=validated_data.get("decisionNodeActive") 

283 ) 

284 

285 decision_node_over = SimulationObject.objects.get( 

286 pk=validated_data.get("decisionNodeOver") 

287 ) 

288 

289 decision_node_active.merge_decision_nodes(decision_node_active, decision_node_over) 

290 

291 return Response({'status': 'success'}, status=200) 

292 

293 except (SimulationObject.DoesNotExist, PropertySet.DoesNotExist) as e: 

294 return Response({'status': 'error', 'message': str(e)}, status=404) 

295 except Exception as e: 

296 return Response({'status': 'error', 'message': str(e),'traceback': traceback.format_exc()}, status=400) 

297 

298 

299 

300 @extend_schema(request=None,responses=int) 

301 @action(detail=True, methods=['POST']) 

302 def add_expression(self, request, pk=None): 

303 """Create a blank expression property on the simulation object.""" 

304 if pk is None: 304 ↛ 305line 304 didn't jump to line 305 because the condition on line 304 was never true

305 return Response({'status': 'error', 'message': 'id is required'}, status=400) 

306 

307 simulation_object = SimulationObject.objects.get(pk=pk) 

308 prop_info = add_expression(simulation_object) 

309 

310 return Response(prop_info.id, status=200) 

311 

312 @extend_schema(request=AddPropertyTemplateSerializer) 

313 @action(detail=True, methods=['POST']) 

314 def add_custom_property_template(self, request, pk=None): 

315 """Attach a custom propertytemplate to the simulation object using the supplied key.""" 

316 if pk is None: 

317 return Response({'status': 'error', 'message': 'id is required'}, status=400) 

318 

319 simulation_object = SimulationObject.objects.prefetch_related( 

320 "properties" 

321 ).get(pk=pk) 

322 serializer = AddPropertyTemplateSerializer(data=request.data) 

323 serializer.is_valid(raise_exception=True) 

324 template_key = serializer.validated_data.get("templateKey") 

325 

326 try: 

327 add_template(simulation_object, template_key) 

328 return Response({'status': 'success'}, status=200) 

329 except ValueError as e: 

330 return Response({'status': 'error', 'message': str(e),'traceback': traceback.format_exc()}, status=400) 

331 

332 

333 @extend_schema(request=RestoreObjectsSerializer, responses=None) 

334 @action(detail=False, methods=['post']) 

335 def restore(self, request): 

336 """Restore previously deleted simulation objects.""" 

337 try: 

338 # Handle both 'ids' and 'object_ids' in request data 

339 if 'ids' in request.data: 

340 request_data = {'object_ids': request.data['ids']} 

341 else: 

342 request_data = request.data 

343 

344 serializer = RestoreObjectsSerializer(data=request_data) 

345 serializer.is_valid(raise_exception=True) 

346 object_ids = serializer.validated_data['object_ids'] 

347 except Exception as e: 

348 return Response( 

349 { 

350 'error': 'Validation error', 

351 'details': str(e), 

352 'traceback': traceback.format_exc(), 

353 'received_data': request.data 

354 }, 

355 status=status.HTTP_400_BAD_REQUEST 

356 ) 

357 

358 if not object_ids: 

359 return Response( 

360 {'error': 'No objectIds provided'}, 

361 status=status.HTTP_400_BAD_REQUEST 

362 ) 

363 

364 DeleteFactory._restore_object_ids(object_ids) 

365 return Response(status=status.HTTP_200_OK) 

366 

367 

368 

369 @extend_schema(request=DuplicateSimulationObjectSerializer, responses=None) 

370 @action(detail=False, methods=['post'], url_path='duplicate-simulation-object') 

371 def duplicate_simulation_object(self, request): 

372 """Duplicate simulation objects and reposition them at the supplied coordinates.""" 

373 try: 

374 serializer = DuplicateSimulationObjectSerializer(data=request.data) 

375 serializer.is_valid(raise_exception=True) 

376 validated_data = serializer.validated_data 

377 duplicator = DuplicateSimulationObject() 

378 flowsheet = self.request.query_params.get("flowsheet") 

379 duplicator.handle_duplication_request( 

380 flowsheet, 

381 validated_data 

382 ) 

383 return Response(None, status=200) 

384 except SimulationObject.DoesNotExist as e: 

385 return Response({'status': 'error', 'message': str(e)}, status=400) 

386 except Exception as e: 

387 return Response({'status': 'error', 'message': str(e),'traceback': traceback.format_exc()}, status=400)